Compare commits

..

41 Commits

Author SHA1 Message Date
dijunkun eeb6a2a1ae [refactor] organize GUI code by responsibility 2026-07-19 21:43:19 +08:00
dijunkun 1f86c43458 [fix] replace clipboard handling with SDL and optimize threading 2026-07-18 23:31:33 +08:00
dijunkun c487d3a62c [fix] refine recent connections overflow layout, refs #85 2026-07-18 22:03:36 +08:00
dijunkun aee5be5ee6 [feat] refine recent connection card controls and tooltip 2026-07-18 20:59:29 +08:00
dijunkun 6ff2783105 [feat] add TURN relay mode configuration 2026-07-18 20:22:36 +08:00
dijunkun d6d30b6842 [fix] fix Windows UAC prompt input by routing Consent UI through secure input helper 2026-07-06 17:47:50 +08:00
dijunkun 5db309243b [fix] fix stuck connecting state when remote device is offline, refs #57 2026-07-05 00:52:21 +08:00
dijunkun 0681f6540d [fix] fix persistent scrollbar in recent connections area when empty, refs #85 2026-06-26 17:29:04 +08:00
dijunkun d843901550 [fix] fix intermittent crash on incoming connection by guarding shared connection status state across threads 2026-06-26 17:21:09 +08:00
dijunkun b41630ebb4 [fix] fix blank connection status popup by handling Gathering state 2026-06-26 16:23:15 +08:00
dijunkun a815eecc73 [fix] fix close-issue workflow env indentation, pagination, and activity time base 2026-06-26 16:14:09 +08:00
dijunkun 7e0984fe9c [chore] refine online status indicator with gradient glow and remove status text 2026-06-26 16:09:21 +08:00
dijunkun 9c28cd2ab2 [fix] fix crash when switching monitors due to race condition and missing bounds checks in screen capturer 2026-06-26 14:44:30 +08:00
dijunkun 3c4000bdbb [fix] add missing Xft development dependency for Linux tray build 2026-06-25 00:35:25 +08:00
dijunkun d8e9fa5bba [feat] support minimizing to the system tray on Linux when closing 2026-06-23 17:11:42 +08:00
dijunkun e026491b9f [feat] support minimizing to the system tray on macOS when closing, refs #87 2026-06-23 17:11:42 +08:00
dijunkun 009699b375 [chore] optimize online status indicator icon 2026-06-22 16:52:51 +08:00
dijunkun 3677588a3d [fix] align recent connection name footer 2026-06-18 10:53:31 +08:00
dijunkun 3d280053a7 [feat] add custom names for recent connection devices and improve panel display 2026-06-17 17:48:17 +08:00
dijunkun fbde3f6a47 [fix] prevent remote keyboard keys from getting stuck 2026-06-15 17:28:34 +08:00
dijunkun 1c1a33fdce [fix] use evdev keycodes for Wayland keyboard input 2026-06-12 11:25:00 +08:00
kunkundi fea238722d [fix] fix macOS remote double-click and triple-click handling, refs #86 2026-06-05 00:25:08 +08:00
kunkundi 178d958c08 [fix] show self-hosted TLS certificate errors in status bar 2026-06-01 00:31:45 +08:00
kunkundi f9633f366b [fix] update MiniRTC: support loading macOS self-hosted TLS roots, refs #79 2026-06-01 00:31:27 +08:00
dijunkun 7a81f3e767 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-05-31 23:21:42 +08:00
dijunkun bbbbbf7927 [fix] update MiniRTC: support loading Windows self-signed root certificates from intermediate stores, refs #79 2026-05-31 23:04:13 +08:00
dijunkun 5735f84008 [ci] include version alias in version.json for compatibility 2026-05-29 11:28:09 +08:00
kunkundi fe0cf42e5d [fix] use Linux system CA paths for update checks 2026-05-29 10:51:22 +08:00
kunkundi 04100584ce [fix] restore latest_version update metadata 2026-05-29 02:11:46 +08:00
kunkundi 9d3a422916 [fix] log update check failures and use macOS trust store 2026-05-29 02:00:13 +08:00
kunkundi 65d8284fb8 [ci] standardize hotfix package version ordering across platforms 2026-05-29 00:52:07 +08:00
kunkundi eea107db66 [chore] adjust settings window button vertical spacing 2026-05-28 23:36:25 +08:00
kunkundi 67812957db [ci] fix GitHub Actions Linux build version parsing 2026-05-28 23:30:42 +08:00
dijunkun 69d77e59cc [fix] make portable Windows Service setup optional, refs #84 2026-05-28 18:59:27 +08:00
dijunkun efcebfd82c [feat] add explicit hotfix patch version support 2026-05-28 15:22:35 +08:00
dijunkun 32345f93bf [fix] validate macOS input display state before injecting events 2026-05-28 04:22:14 +08:00
dijunkun 193e4bd5bf [fix] guard macOS speaker capture callbacks against invalid buffers 2026-05-28 04:22:07 +08:00
dijunkun 53edf3d57e [fix] harden macOS screen capture lifecycle against late callbacks 2026-05-28 04:21:59 +08:00
dijunkun 895e297771 [fix] initialize render runtime state to avoid invalid SDL handles 2026-05-28 04:21:52 +08:00
dijunkun 8f3959e6c6 [fix] reset stale macOS permissions during install 2026-05-28 01:34:02 +08:00
dijunkun 5ff6b601c7 [fix] improve macOS permission request flow 2026-05-28 01:33:50 +08:00
113 changed files with 12664 additions and 7233 deletions
+146 -22
View File
@@ -7,6 +7,11 @@ on:
tags: tags:
- "*" - "*"
workflow_dispatch: workflow_dispatch:
inputs:
patch:
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
required: false
default: "0"
permissions: permissions:
contents: write contents: write
@@ -37,21 +42,53 @@ jobs:
steps: steps:
- name: Extract version number - name: Extract version number
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}" VERSION_BASE="${VERSION_REF#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV PATCH_NUMBER="${{ github.event.inputs.patch }}"
BUILD_DATE_OVERRIDE=""
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
- name: Set legal Debian version - name: Set legal Debian version
shell: bash shell: bash
run: | run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) BUILD_DATE="${BUILD_DATE_OVERRIDE}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) if [[ -z "${BUILD_DATE}" ]]; then
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
fi
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" VERSION_BASE="0.0.0-${VERSION_BASE}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
fi fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
@@ -67,6 +104,7 @@ jobs:
CUDA_PATH: /usr/local/cuda CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data XMAKE_GLOBALDIR: /data
run: | run: |
apt install -y libxft-dev
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk xmake b -vy --root crossdesk
@@ -102,14 +140,43 @@ jobs:
steps: steps:
- name: Extract version number - name: Extract version number
id: version id: version
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}" PATCH_NUMBER="${{ github.event.inputs.patch }}"
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
fi
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}" echo "VERSION_NUM=${VERSION_NUM}"
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
@@ -163,10 +230,38 @@ jobs:
$version = $ref -replace '^refs/(tags|heads)/', '' $version = $ref -replace '^refs/(tags|heads)/', ''
$version = $version -replace '^v', '' $version = $version -replace '^v', ''
$version = $version -replace '/', '-' $version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd") $BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV $PATCH_NUMBER = "${{ github.event.inputs.patch }}"
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
$PATCH_NUMBER = "0"
}
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
$version = $Matches[1]
$PATCH_NUMBER = $Matches[3]
$BUILD_DATE = $Matches[4]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
$version = $Matches[1]
$BUILD_DATE = $Matches[3]
$PATCH_NUMBER = $Matches[4]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
$version = $Matches[1]
$BUILD_DATE = $Matches[3]
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
$version = $Matches[1]
$PATCH_NUMBER = $Matches[3]
}
if ($PATCH_NUMBER -ne "0") {
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
} else {
$VERSION_NUM = "v$version-$BUILD_DATE"
}
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
@@ -239,8 +334,7 @@ jobs:
- name: Package - name: Package
shell: pwsh shell: pwsh
run: | run: |
cd "${{ github.workspace }}\scripts\windows" & "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
- name: Build Portable CrossDesk - name: Build Portable CrossDesk
run: | run: |
@@ -310,19 +404,47 @@ jobs:
- name: Extract version number - name: Extract version number
id: version id: version
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d) PATCH_NUMBER="${{ github.event.inputs.patch }}"
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
PATCH_NUMBER=0
fi
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
BUILD_DATE="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
BUILD_DATE="${BASH_REMATCH[3]}"
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
fi
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
if [[ "${PATCH_NUMBER}" != "0" ]]; then
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
fi
VERSION_WITH_V="v${VERSION_NUM}" VERSION_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Rename artifacts - name: Rename artifacts
run: | run: |
@@ -380,8 +502,10 @@ jobs:
run: | run: |
cat > version.json << EOF cat > version.json << EOF
{ {
"version": "${{ steps.version.outputs.VERSION_ONLY }}", "latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
"version": "${{ steps.version.outputs.VERSION_NUM }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": "", "releaseName": "",
"releaseNotes": "", "releaseNotes": "",
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}", "tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
+14 -25
View File
@@ -4,6 +4,7 @@ on:
schedule: schedule:
# run every day at midnight # run every day at midnight
- cron: "0 0 * * *" - cron: "0 0 * * *"
workflow_dispatch:
permissions: permissions:
issues: write issues: write
@@ -15,19 +16,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check inactive issues and close them - name: Check inactive issues and close them
uses: actions/github-script@v6 uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
script: | script: |
const { data: issues } = await github.rest.issues.listForRepo({ const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
const now = Date.now();
// paginate through all open issues (listForRepo also returns PRs)
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
state: 'open', state: 'open',
per_page: 100,
}); });
const now = new Date().getTime();
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
for (const issue of issues) { for (const issue of issues) {
// skip pull requests (they are also returned by listForRepo) // skip pull requests (they are also returned by listForRepo)
if (issue.pull_request) continue; if (issue.pull_request) continue;
@@ -38,26 +41,14 @@ jobs:
continue; continue;
} }
// fetch comments for this issue // last activity time = the issue's own updated_at, which is
const { data: comments } = await github.rest.issues.listComments({ // refreshed on comments, labels, etc. This avoids relying on
owner: context.repo.owner, // fetching comments and is accurate even when comments are edited.
repo: context.repo.repo, const lastActivityTime = new Date(issue.updated_at).getTime();
issue_number: issue.number,
per_page: 100,
});
// determine the "last activity" time
let lastActivityTime;
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
lastActivityTime = new Date(lastComment.updated_at).getTime();
} else {
lastActivityTime = new Date(issue.created_at).getTime();
}
// check inactivity // check inactivity
if (now - lastActivityTime > inactivePeriod) { if (now - lastActivityTime > inactivePeriod) {
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`); console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner: context.repo.owner, owner: context.repo.owner,
@@ -76,5 +67,3 @@ jobs:
console.log(`Skipping issue #${issue.number} (Active within 7 days).`); console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
} }
} }
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+27 -7
View File
@@ -20,21 +20,39 @@ jobs:
- name: Extract version from tag - name: Extract version from tag
id: version id: version
shell: bash
run: | run: |
TAG_NAME="${{ github.event.release.tag_name }}" TAG_NAME="${{ github.event.release.tag_name }}"
VERSION_ONLY="${TAG_NAME#v}" TAG_VERSION="${TAG_NAME#v}"
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT VERSION_FULL="${TAG_VERSION}"
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT VERSION_BASE="${TAG_VERSION}"
PATCH_NUMBER=0
# Extract date from tag if available (format: v1.2.3-20251113-abc) # Extract date and patch from tags such as v1.2.3-1-20251113.
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
DATE_STR="${BASH_REMATCH[1]}" VERSION_BASE="${BASH_REMATCH[1]}"
PATCH_NUMBER="${BASH_REMATCH[3]}"
DATE_STR="${BASH_REMATCH[4]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
DATE_STR="${BASH_REMATCH[3]}"
PATCH_NUMBER="${BASH_REMATCH[4]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
VERSION_BASE="${BASH_REMATCH[1]}"
DATE_STR="${BASH_REMATCH[3]}"
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
else else
# Use release published date # Use release published date
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1) BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
fi fi
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Install jq - name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq run: sudo apt-get update && sudo apt-get install -y jq
@@ -122,8 +140,10 @@ jobs:
# Generate version.json using cat and heredoc # Generate version.json using cat and heredoc
cat > version.json << EOF cat > version.json << EOF
{ {
"version": "${{ steps.version.outputs.VERSION_ONLY }}", "latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
"version": "${{ steps.version.outputs.VERSION_FULL }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }}, "releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }}, "releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
"tagName": "${{ steps.version.outputs.TAG_NAME }}", "tagName": "${{ steps.version.outputs.TAG_NAME }}",
+207
View File
@@ -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/` |
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
PKG_NAME="crossdesk" PKG_NAME="crossdesk"
APP_NAME="CrossDesk" APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="amd64" ARCHITECTURE="amd64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>" MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client." DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64" ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit) # Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}" DEB_VERSION="${APP_VERSION#v}"
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
PKG_NAME="crossdesk" PKG_NAME="crossdesk"
APP_NAME="CrossDesk" APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="arm64" ARCHITECTURE="arm64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>" MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client." DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64" ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
# Remove 'v' prefix from version for Debian package (Debian version must start with digit) # Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}" DEB_VERSION="${APP_VERSION#v}"
+84 -50
View File
@@ -4,17 +4,36 @@ set -e
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk" EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos" PLATFORM="macos"
ARCH="arm64" ARCH="arm64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="10.12"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources" RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg" PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg" DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
@@ -73,67 +92,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
</plist> </plist>
EOF EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
echo ".app created successfully." echo ".app created successfully."
mkdir -p build_pkg_scripts
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
chmod +x build_pkg_scripts/postinstall
mkdir -p build_pkg_resources
cat > build_pkg_resources/welcome.html <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #222;
margin: 0;
padding: 0;
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 12px;
}
p {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
</body>
</html>
EOF
echo "building pkg..." echo "building pkg..."
pkgbuild \ pkgbuild \
--identifier "${IDENTIFIER}" \ --identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \ --version "${APP_VERSION}" \
--install-location "/Applications" \ --install-location "/Applications" \
--component "${APP_BUNDLE}" \ --component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p build_pkg_scripts cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
cat > build_pkg_scripts/postinstall <<'EOF' <installer-gui-script minSpecVersion="1">
#!/bin/bash <title>${INSTALLER_TITLE}</title>
set -e <welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
IDENTIFIER="cn.crossdesk.app" <choices-outline>
<line choice="default">
# 获取当前登录用户 <line choice="${IDENTIFIER}"/>
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console ) </line>
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' ) </choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
# 清除应用的权限授权,以便重新授权 <choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
# 使用 tccutil 重置录屏权限和辅助功能权限 <pkg-ref id="${IDENTIFIER}"/>
if command -v tccutil >/dev/null 2>&1; then </choice>
# 重置录屏权限 <pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true </installer-gui-script>
# 重置辅助功能权限
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
# 重置摄像头权限(如果需要)
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
# 重置麦克风权限(如果需要)
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
fi
# 为所有用户清除权限(可选,如果需要)
# 遍历所有用户目录并清除权限
for USER_DIR in /Users/*; do
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
USER_NAME=$(basename "$USER_DIR")
# 跳过系统用户
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
# 删除 TCC 数据库中的相关条目(需要管理员权限)
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
if [ -f "$TCC_DB" ]; then
# 使用 sqlite3 删除相关权限记录(如果可用)
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
fi
fi
fi
fi
done
exit 0
EOF EOF
chmod +x build_pkg_scripts/postinstall
productbuild \ productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \ --distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
"${PKG_NAME}" "${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}" echo "PKG package created: ${PKG_NAME}"
@@ -171,7 +205,7 @@ APPLESCRIPT
fi fi
echo "Set icon finished" echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE} rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
echo "PKG package created successfully." echo "PKG package created successfully."
echo "package ${APP_BUNDLE}" echo "package ${APP_BUNDLE}"
+84 -50
View File
@@ -4,17 +4,36 @@ set -e
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk" EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos" PLATFORM="macos"
ARCH="x64" ARCH="x64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" MACOS_MIN_VERSION="10.12"
normalize_app_version() {
local input="$1"
local prefix=""
local body="$input"
if [[ "$body" == v* ]]; then
prefix="v"
body="${body#v}"
fi
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
else
echo "$input"
fi
}
APP_VERSION="$(normalize_app_version "$1")"
APP_BUNDLE="${APP_NAME_UPPER}.app" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources" RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg" PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg" DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
@@ -73,67 +92,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
</plist> </plist>
EOF EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
echo ".app created successfully." echo ".app created successfully."
mkdir -p build_pkg_scripts
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
chmod +x build_pkg_scripts/postinstall
mkdir -p build_pkg_resources
cat > build_pkg_resources/welcome.html <<EOF
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
font-size: 13px;
line-height: 1.5;
color: #222;
margin: 0;
padding: 0;
}
h1 {
font-size: 20px;
font-weight: 600;
margin: 0 0 12px;
}
p {
margin: 0 0 10px;
}
</style>
</head>
<body>
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
</body>
</html>
EOF
echo "building pkg..." echo "building pkg..."
pkgbuild \ pkgbuild \
--identifier "${IDENTIFIER}" \ --identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \ --version "${APP_VERSION}" \
--install-location "/Applications" \ --install-location "/Applications" \
--component "${APP_BUNDLE}" \ --component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p build_pkg_scripts cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
cat > build_pkg_scripts/postinstall <<'EOF' <installer-gui-script minSpecVersion="1">
#!/bin/bash <title>${INSTALLER_TITLE}</title>
set -e <welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
IDENTIFIER="cn.crossdesk.app" <choices-outline>
<line choice="default">
# 获取当前登录用户 <line choice="${IDENTIFIER}"/>
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console ) </line>
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' ) </choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
# 清除应用的权限授权,以便重新授权 <choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
# 使用 tccutil 重置录屏权限和辅助功能权限 <pkg-ref id="${IDENTIFIER}"/>
if command -v tccutil >/dev/null 2>&1; then </choice>
# 重置录屏权限 <pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true </installer-gui-script>
# 重置辅助功能权限
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
# 重置摄像头权限(如果需要)
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
# 重置麦克风权限(如果需要)
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
fi
# 为所有用户清除权限(可选,如果需要)
# 遍历所有用户目录并清除权限
for USER_DIR in /Users/*; do
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
USER_NAME=$(basename "$USER_DIR")
# 跳过系统用户
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
# 删除 TCC 数据库中的相关条目(需要管理员权限)
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
if [ -f "$TCC_DB" ]; then
# 使用 sqlite3 删除相关权限记录(如果可用)
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
fi
fi
fi
fi
done
exit 0
EOF EOF
chmod +x build_pkg_scripts/postinstall
productbuild \ productbuild \
--package build_pkg_temp/${APP_NAME}-component.pkg \ --distribution build_pkg_temp/Distribution \
--package-path build_pkg_temp \
--resources build_pkg_resources \
"${PKG_NAME}" "${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}" echo "PKG package created: ${PKG_NAME}"
@@ -171,7 +205,7 @@ APPLESCRIPT
fi fi
echo "Set icon finished" echo "Set icon finished"
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE} rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
echo "PKG package created successfully." echo "PKG package created successfully."
echo "package ${APP_BUNDLE}" echo "package ${APP_BUNDLE}"
+112
View File
@@ -0,0 +1,112 @@
#!/bin/bash
set -e
APP_IDENTIFIER="cn.crossdesk.app"
# Keep known historical identifiers here. tccutil only resets identifiers that
# Launch Services can currently resolve, so path/db cleanup below remains a
# best-effort fallback for stale entries from unsigned or removed builds.
BUNDLE_IDENTIFIERS=(
"cn.crossdesk.app"
"cn.crossdesk.CrossDesk"
"com.crossdesk.app"
"com.crossdesk.CrossDesk"
"com.kunkundi.crossdesk"
"com.kunkundi.CrossDesk"
)
TCC_SERVICES=(
"ScreenCapture"
"Accessibility"
"Microphone"
"AudioCapture"
)
run_tccutil() {
local user_name="$1"
local user_id="$2"
local service="$3"
local bundle_id="$4"
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
/bin/launchctl asuser "$user_id" \
/usr/bin/sudo -u "$user_name" \
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
else
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
fi
}
reset_bundle_tcc() {
local user_name="$1"
local user_id="$2"
local bundle_id
local service
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
continue
fi
for service in "${TCC_SERVICES[@]}"; do
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
done
done
}
cleanup_tcc_db() {
local db_path="$1"
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
return
fi
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
DELETE FROM access
WHERE service IN (
'kTCCServiceScreenCapture',
'kTCCServiceAccessibility',
'kTCCServiceMicrophone',
'kTCCServiceAudioCapture'
)
AND (
client IN (
'cn.crossdesk.app',
'cn.crossdesk.CrossDesk',
'com.crossdesk.app',
'com.crossdesk.CrossDesk',
'com.kunkundi.crossdesk',
'com.kunkundi.CrossDesk'
)
OR lower(client) LIKE '%crossdesk%'
);
SQL
}
cleanup_user_tcc_db() {
local user_name="$1"
local home_dir
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
/usr/bin/awk '{print $2}')
if [ -z "$home_dir" ]; then
return
fi
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
}
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
if [ -n "$CONSOLE_USER" ] &&
[ "$CONSOLE_USER" != "root" ] &&
[ "$CONSOLE_USER" != "loginwindow" ]; then
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
cleanup_user_tcc_db "$CONSOLE_USER"
fi
# Also clear any system/root-scoped decisions as a harmless fallback.
reset_bundle_tcc "" ""
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
exit 0
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
name="CrossDesk"
type="win32" />
<description>CrossDesk Portable Application</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
+8
View File
@@ -0,0 +1,8 @@
// Portable build resource. The app itself runs as the current user; service
// installation raises a separate UAC prompt only when the user chooses it.
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
#define RT_MANIFEST 24
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
+40
View File
@@ -0,0 +1,40 @@
param(
[Parameter(Mandatory = $true)]
[string]$Version
)
$ErrorActionPreference = "Stop"
function Normalize-AppVersion {
param(
[Parameter(Mandatory = $true)]
[string]$InputVersion
)
$prefix = ""
$body = $InputVersion
if ($body.StartsWith("v")) {
$prefix = "v"
$body = $body.Substring(1)
}
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
}
return $InputVersion
}
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Push-Location $scriptDir
try {
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
} finally {
Pop-Location
}
+5 -1
View File
@@ -128,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
if (pid > 0) _exit(0); if (pid > 0) _exit(0);
umask(0); umask(0);
chdir("/"); if (chdir("/") != 0) {
std::cerr << "Failed to change daemon working directory to /: "
<< std::strerror(errno) << std::endl;
return false;
}
// redirect file descriptors: keep stdout/stderr if from terminal, else // redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null // redirect to /dev/null
+85 -6
View File
@@ -5,6 +5,15 @@
namespace crossdesk { 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) ConfigCenter::ConfigCenter(const std::string& config_path)
: config_path_(config_path) { : config_path_(config_path) {
ini_.SetUnicode(true); ini_.SetUnicode(true);
@@ -20,6 +29,8 @@ int ConfigCenter::Load() {
return -1; return -1;
} }
bool persist_turn_mode_migration = false;
const long language_value = const long language_value =
ini_.GetLongValue(section_, "language", static_cast<long>(language_)); ini_.GetLongValue(section_, "language", static_cast<long>(language_));
if (language_value < static_cast<long>(LANGUAGE::CHINESE) || 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_ = ini_.GetBoolValue(section_, "hardware_video_codec",
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_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
enable_self_hosted_ = enable_self_hosted_ =
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_); ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -79,6 +108,9 @@ int ConfigCenter::Load() {
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_); enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue( enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_); section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
portable_service_prompt_suppressed_ =
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
const char* file_transfer_save_path_value = const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr); ini_.GetValue(section_, "file_transfer_save_path", nullptr);
@@ -89,6 +121,11 @@ int ConfigCenter::Load() {
file_transfer_save_path_ = ""; file_transfer_save_path_ = "";
} }
if (persist_turn_mode_migration &&
ini_.SaveFile(config_path_.c_str()) < 0) {
return -1;
}
return 0; return 0;
} }
@@ -101,7 +138,9 @@ int ConfigCenter::Save() {
ini_.SetLongValue(section_, "video_encode_format", ini_.SetLongValue(section_, "video_encode_format",
static_cast<long>(video_encode_format_)); static_cast<long>(video_encode_format_));
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_); 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_srtp", enable_srtp_);
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_); ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
@@ -118,6 +157,8 @@ int ConfigCenter::Save() {
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_); ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray", ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_); enable_minimize_to_tray_);
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
ini_.SetValue(section_, "file_transfer_save_path", ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str()); file_transfer_save_path_.c_str());
@@ -186,9 +227,15 @@ int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
return 0; return 0;
} }
int ConfigCenter::SetTurn(bool enable_turn) { int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
enable_turn_ = enable_turn; if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
ini_.SetBoolValue(section_, "enable_turn", enable_turn_); 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()); SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) { if (rc < 0) {
return -1; return -1;
@@ -196,6 +243,16 @@ int ConfigCenter::SetTurn(bool enable_turn) {
return 0; 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) { int ConfigCenter::SetSrtp(bool enable_srtp) {
enable_srtp_ = enable_srtp; enable_srtp_ = enable_srtp;
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_); ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
@@ -325,6 +382,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
return 0; return 0;
} }
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
portable_service_prompt_suppressed_ = suppressed;
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
portable_service_prompt_suppressed_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// getters // getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
@@ -345,7 +414,13 @@ bool ConfigCenter::IsHardwareVideoCodec() const {
return hardware_video_codec_; 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_; } bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
@@ -377,6 +452,10 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; } bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
return portable_service_prompt_suppressed_;
}
int ConfigCenter::SetFileTransferSavePath(const std::string& path) { int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path; file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path", ini_.SetValue(section_, "file_transfer_save_path",
+12 -1
View File
@@ -19,6 +19,12 @@ class ConfigCenter {
enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 }; enum class VIDEO_QUALITY { LOW = 0, MEDIUM = 1, HIGH = 2 };
enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 }; enum class VIDEO_FRAME_RATE { FPS_30 = 0, FPS_60 = 1 };
enum class VIDEO_ENCODE_FORMAT { H264 = 0, AV1 = 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: public:
explicit ConfigCenter(const std::string& config_path = "config.ini"); explicit ConfigCenter(const std::string& config_path = "config.ini");
@@ -30,6 +36,7 @@ class ConfigCenter {
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate); int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format); int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
int SetHardwareVideoCodec(bool hardware_video_codec); int SetHardwareVideoCodec(bool hardware_video_codec);
int SetTurnMode(TURN_MODE turn_mode);
int SetTurn(bool enable_turn); int SetTurn(bool enable_turn);
int SetSrtp(bool enable_srtp); int SetSrtp(bool enable_srtp);
int SetServerHost(const std::string& signal_server_host); int SetServerHost(const std::string& signal_server_host);
@@ -39,6 +46,7 @@ class ConfigCenter {
int SetMinimizeToTray(bool enable_minimize_to_tray); int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart); int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon); int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
int SetFileTransferSavePath(const std::string& path); int SetFileTransferSavePath(const std::string& path);
// read config // read config
@@ -48,6 +56,7 @@ class ConfigCenter {
VIDEO_FRAME_RATE GetVideoFrameRate() const; VIDEO_FRAME_RATE GetVideoFrameRate() const;
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const; VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
bool IsHardwareVideoCodec() const; bool IsHardwareVideoCodec() const;
TURN_MODE GetTurnMode() const;
bool IsEnableTurn() const; bool IsEnableTurn() const;
bool IsEnableSrtp() const; bool IsEnableSrtp() const;
std::string GetSignalServerHost() const; std::string GetSignalServerHost() const;
@@ -60,6 +69,7 @@ class ConfigCenter {
bool IsMinimizeToTray() const; bool IsMinimizeToTray() const;
bool IsEnableAutostart() const; bool IsEnableAutostart() const;
bool IsEnableDaemon() const; bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
std::string GetFileTransferSavePath() const; std::string GetFileTransferSavePath() const;
int Load(); int Load();
@@ -75,7 +85,7 @@ class ConfigCenter {
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60; VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264; VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false; bool hardware_video_codec_ = false;
bool enable_turn_ = true; TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
bool enable_srtp_ = false; bool enable_srtp_ = false;
std::string signal_server_host_ = ""; std::string signal_server_host_ = "";
std::string signal_server_host_default_ = "api.crossdesk.cn"; std::string signal_server_host_default_ = "api.crossdesk.cn";
@@ -87,6 +97,7 @@ class ConfigCenter {
bool enable_minimize_to_tray_ = false; bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false; bool enable_autostart_ = false;
bool enable_daemon_ = false; bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
std::string file_transfer_save_path_ = ""; std::string file_transfer_save_path_ = "";
}; };
} // namespace crossdesk } // namespace crossdesk
+63 -6
View File
@@ -21,12 +21,13 @@ namespace crossdesk {
typedef enum { typedef enum {
mouse = 0, mouse = 0,
keyboard, keyboard = 1,
audio_capture, audio_capture = 2,
host_infomation, host_infomation = 3,
display_id, display_id = 4,
service_status, service_status = 5,
service_command, service_command = 6,
keyboard_state = 7,
} ControlType; } ControlType;
typedef enum { typedef enum {
move = 0, move = 0,
@@ -55,6 +56,20 @@ typedef struct {
KeyFlag flag; KeyFlag flag;
} Key; } Key;
inline constexpr size_t kMaxKeyboardStateKeys = 32;
typedef struct {
size_t key_value;
uint32_t scan_code;
bool extended;
} KeyboardStateKey;
typedef struct {
uint32_t seq;
size_t pressed_count;
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
typedef struct { typedef struct {
char host_name[64]; char host_name[64];
size_t host_name_size; size_t host_name_size;
@@ -80,6 +95,7 @@ struct RemoteAction {
union { union {
Mouse m; Mouse m;
Key k; Key k;
KeyboardState ks;
HostInfo i; HostInfo i;
bool a; bool a;
int d; int d;
@@ -111,6 +127,20 @@ struct RemoteAction {
{"extended", a.k.extended}, {"extended", a.k.extended},
{"flag", a.k.flag}}; {"flag", a.k.flag}};
break; break;
case ControlType::keyboard_state: {
json keys = json::array();
const size_t pressed_count =
a.ks.pressed_count < kMaxKeyboardStateKeys
? a.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
{"scan_code", a.ks.pressed_keys[idx].scan_code},
{"extended", a.ks.pressed_keys[idx].extended}});
}
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::audio_capture: case ControlType::audio_capture:
j["audio_capture"] = a.a; j["audio_capture"] = a.a;
break; break;
@@ -162,6 +192,33 @@ struct RemoteAction {
out.k.extended = j.at("keyboard").value("extended", false); out.k.extended = j.at("keyboard").value("extended", false);
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>(); out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break; break;
case ControlType::keyboard_state: {
const auto& keyboard_state_json = j.at("keyboard_state");
out.ks.seq = keyboard_state_json.value("seq", 0u);
out.ks.pressed_count = 0;
const auto keys_json =
keyboard_state_json.value("pressed_keys", json::array());
if (!keys_json.is_array()) {
break;
}
const size_t count =
keys_json.size() < kMaxKeyboardStateKeys
? keys_json.size()
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < count; ++idx) {
const auto& key_json = keys_json[idx];
out.ks.pressed_keys[idx].key_value =
key_json.at("key_value").get<size_t>();
out.ks.pressed_keys[idx].scan_code =
key_json.value("scan_code", static_cast<uint32_t>(0));
out.ks.pressed_keys[idx].extended =
key_json.value("extended", false);
}
out.ks.pressed_count = count;
break;
}
case ControlType::audio_capture: case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>(); out.a = j.at("audio_capture").get<bool>();
break; break;
@@ -8,6 +8,7 @@
#include <dbus/dbus.h> #include <dbus/dbus.h>
#endif #endif
#include "linux_evdev_keycode.h"
#include "rd_log.h" #include "rd_log.h"
#include "wayland_portal_shared.h" #include "wayland_portal_shared.h"
@@ -579,33 +580,46 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, uint32_t scan_code,
bool extended) { bool extended) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER #if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
(void)scan_code;
(void)extended;
if (!dbus_connection_ || wayland_session_handle_.empty()) { if (!dbus_connection_ || wayland_session_handle_.empty()) {
return -1; return -1;
} }
const auto key_it = vkCodeToX11KeySym.find(key_code); const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
if (key_it == vkCodeToX11KeySym.end()) {
const int evdev_keycode =
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
if (evdev_keycode >= 0 &&
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
return 0; return 0;
} }
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased; const auto key_it = vkCodeToX11KeySym.find(key_code);
const int keysym = key_it->second; if (key_it == vkCodeToX11KeySym.end()) {
if (evdev_keycode >= 0) {
LOG_ERROR(
"Failed to send Wayland keyboard keycode event, vk_code={}, "
"evdev_keycode={}, is_down={}",
key_code, evdev_keycode, is_down);
return -3;
}
return 0;
}
// Prefer keycode injection to preserve physical-key semantics and avoid // Prefer keycode injection to preserve physical-key semantics and avoid
// implicit Shift interpretation for uppercase keysyms. // implicit Shift interpretation for uppercase keysyms.
if (display_) { if (display_) {
const int keysym = key_it->second;
const KeyCode x11_keycode = const KeyCode x11_keycode =
XKeysymToKeycode(display_, static_cast<KeySym>(keysym)); XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
if (x11_keycode > 8) { if (x11_keycode > 8) {
const int evdev_keycode = static_cast<int>(x11_keycode) - 8; const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) { if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
return 0; return 0;
} }
} }
} }
const int keysym = key_it->second;
const int fallback_keysym = NormalizeFallbackKeysym(keysym); const int fallback_keysym = NormalizeFallbackKeysym(keysym);
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) { if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
return 0; return 0;
@@ -310,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
if (IsFunctionKey(cg_key_code) && !is_down) { if (IsFunctionKey(cg_key_code) && !is_down) {
CGEventRef fn_release_event = CGEventRef fn_release_event =
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false); CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
if (!fn_release_event) {
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1;
}
CGEventPost(kCGHIDEventTap, fn_release_event); CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event); CFRelease(fn_release_event);
} }
+295
View File
@@ -0,0 +1,295 @@
#ifndef _LINUX_EVDEV_KEYCODE_H_
#define _LINUX_EVDEV_KEYCODE_H_
#include <linux/input-event-codes.h>
#include <cstdint>
namespace crossdesk {
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
bool extended) {
const uint32_t base_scan_code = scan_code & 0xFFu;
if (base_scan_code == 0) {
return -1;
}
if (extended) {
switch (base_scan_code) {
case 0x1C:
return KEY_KPENTER;
case 0x1D:
return KEY_RIGHTCTRL;
case 0x35:
return KEY_KPSLASH;
case 0x38:
return KEY_RIGHTALT;
case 0x47:
return KEY_HOME;
case 0x48:
return KEY_UP;
case 0x49:
return KEY_PAGEUP;
case 0x4B:
return KEY_LEFT;
case 0x4D:
return KEY_RIGHT;
case 0x4F:
return KEY_END;
case 0x50:
return KEY_DOWN;
case 0x51:
return KEY_PAGEDOWN;
case 0x52:
return KEY_INSERT;
case 0x53:
return KEY_DELETE;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
default:
return -1;
}
}
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
// with the low byte of the Windows scan code.
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
base_scan_code == 0x58) {
return static_cast<int>(base_scan_code);
}
return -1;
}
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
switch (key_code) {
case 0x08:
return KEY_BACKSPACE;
case 0x09:
return KEY_TAB;
case 0x0D:
return KEY_ENTER;
case 0x10:
case 0xA0:
return KEY_LEFTSHIFT;
case 0x11:
case 0xA2:
return KEY_LEFTCTRL;
case 0x12:
case 0xA4:
return KEY_LEFTALT;
case 0x13:
return KEY_PAUSE;
case 0x14:
return KEY_CAPSLOCK;
case 0x1B:
return KEY_ESC;
case 0x20:
return KEY_SPACE;
case 0x21:
return KEY_PAGEUP;
case 0x22:
return KEY_PAGEDOWN;
case 0x23:
return KEY_END;
case 0x24:
return KEY_HOME;
case 0x25:
return KEY_LEFT;
case 0x26:
return KEY_UP;
case 0x27:
return KEY_RIGHT;
case 0x28:
return KEY_DOWN;
case 0x2C:
return KEY_SYSRQ;
case 0x2D:
return KEY_INSERT;
case 0x2E:
return KEY_DELETE;
case 0x30:
return KEY_0;
case 0x31:
return KEY_1;
case 0x32:
return KEY_2;
case 0x33:
return KEY_3;
case 0x34:
return KEY_4;
case 0x35:
return KEY_5;
case 0x36:
return KEY_6;
case 0x37:
return KEY_7;
case 0x38:
return KEY_8;
case 0x39:
return KEY_9;
case 0x41:
return KEY_A;
case 0x42:
return KEY_B;
case 0x43:
return KEY_C;
case 0x44:
return KEY_D;
case 0x45:
return KEY_E;
case 0x46:
return KEY_F;
case 0x47:
return KEY_G;
case 0x48:
return KEY_H;
case 0x49:
return KEY_I;
case 0x4A:
return KEY_J;
case 0x4B:
return KEY_K;
case 0x4C:
return KEY_L;
case 0x4D:
return KEY_M;
case 0x4E:
return KEY_N;
case 0x4F:
return KEY_O;
case 0x50:
return KEY_P;
case 0x51:
return KEY_Q;
case 0x52:
return KEY_R;
case 0x53:
return KEY_S;
case 0x54:
return KEY_T;
case 0x55:
return KEY_U;
case 0x56:
return KEY_V;
case 0x57:
return KEY_W;
case 0x58:
return KEY_X;
case 0x59:
return KEY_Y;
case 0x5A:
return KEY_Z;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
case 0x60:
return KEY_KP0;
case 0x61:
return KEY_KP1;
case 0x62:
return KEY_KP2;
case 0x63:
return KEY_KP3;
case 0x64:
return KEY_KP4;
case 0x65:
return KEY_KP5;
case 0x66:
return KEY_KP6;
case 0x67:
return KEY_KP7;
case 0x68:
return KEY_KP8;
case 0x69:
return KEY_KP9;
case 0x6A:
return KEY_KPASTERISK;
case 0x6B:
return KEY_KPPLUS;
case 0x6D:
return KEY_KPMINUS;
case 0x6E:
return KEY_KPDOT;
case 0x6F:
return KEY_KPSLASH;
case 0x70:
return KEY_F1;
case 0x71:
return KEY_F2;
case 0x72:
return KEY_F3;
case 0x73:
return KEY_F4;
case 0x74:
return KEY_F5;
case 0x75:
return KEY_F6;
case 0x76:
return KEY_F7;
case 0x77:
return KEY_F8;
case 0x78:
return KEY_F9;
case 0x79:
return KEY_F10;
case 0x7A:
return KEY_F11;
case 0x7B:
return KEY_F12;
case 0x90:
return KEY_NUMLOCK;
case 0x91:
return KEY_SCROLLLOCK;
case 0xA1:
return KEY_RIGHTSHIFT;
case 0xA3:
return KEY_RIGHTCTRL;
case 0xA5:
return KEY_RIGHTALT;
case 0xBA:
return KEY_SEMICOLON;
case 0xBB:
return KEY_EQUAL;
case 0xBC:
return KEY_COMMA;
case 0xBD:
return KEY_MINUS;
case 0xBE:
return KEY_DOT;
case 0xBF:
return KEY_SLASH;
case 0xC0:
return KEY_GRAVE;
case 0xDB:
return KEY_LEFTBRACE;
case 0xDC:
return KEY_BACKSLASH;
case 0xDD:
return KEY_RIGHTBRACE;
case 0xDE:
return KEY_APOSTROPHE;
default:
return -1;
}
}
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
uint32_t scan_code,
bool extended) {
const int scan_keycode =
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
if (scan_keycode >= 0) {
return scan_keycode;
}
return LinuxEvdevKeycodeFromWindowsVk(key_code);
}
} // namespace crossdesk
#endif
@@ -2,9 +2,35 @@
#include <ApplicationServices/ApplicationServices.h> #include <ApplicationServices/ApplicationServices.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include "rd_log.h" #include "rd_log.h"
namespace crossdesk { 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() {} MouseController::MouseController() {}
@@ -18,85 +44,142 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
int MouseController::Destroy() { return 0; } 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 MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) { int display_index) {
int mouse_pos_x = if (remote_action.type != ControlType::mouse) {
remote_action.m.x * display_info_list_[display_index].width + return 0;
display_info_list_[display_index].left; }
int mouse_pos_y =
remote_action.m.y * display_info_list_[display_index].height +
display_info_list_[display_index].top;
if (remote_action.type == ControlType::mouse) { if (display_index < 0 ||
CGEventRef mouse_event = nullptr; display_index >= static_cast<int>(display_info_list_.size())) {
CGEventType mouse_type; LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
CGMouseButton mouse_button; display_index, display_info_list_.size());
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y); return -1;
}
switch (remote_action.m.flag) { const DisplayInfo& display_info = display_info_list_[display_index];
case MouseFlag::left_down: if (display_info.width <= 0 || display_info.height <= 0) {
mouse_type = kCGEventLeftMouseDown; LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
left_dragging_ = true; display_info.width, display_info.height);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, return -1;
kCGMouseButtonLeft); }
break;
case MouseFlag::left_up:
mouse_type = kCGEventLeftMouseUp;
left_dragging_ = false;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
break;
case MouseFlag::right_down:
mouse_type = kCGEventRightMouseDown;
right_dragging_ = true;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
break;
case MouseFlag::right_up:
mouse_type = kCGEventRightMouseUp;
right_dragging_ = false;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
break;
case MouseFlag::middle_down:
mouse_type = kCGEventOtherMouseDown;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
break;
case MouseFlag::middle_up:
mouse_type = kCGEventOtherMouseUp;
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
break;
case MouseFlag::wheel_vertical:
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);
break;
default:
if (left_dragging_) {
mouse_type = kCGEventLeftMouseDragged;
mouse_button = kCGMouseButtonLeft;
} else if (right_dragging_) {
mouse_type = kCGEventRightMouseDragged;
mouse_button = kCGMouseButtonRight;
} else {
mouse_type = kCGEventMouseMoved;
mouse_button = kCGMouseButtonLeft;
}
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
mouse_button); const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
break; int mouse_pos_x = normalized_x * display_info.width + display_info.left;
} int mouse_pos_y = normalized_y * display_info.height + display_info.top;
if (mouse_event) { CGEventRef mouse_event = nullptr;
CGEventPost(kCGHIDEventTap, mouse_event); CGEventType mouse_type;
CFRelease(mouse_event); 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);
break;
case MouseFlag::wheel_horizontal:
mouse_event = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine,
2, 0, remote_action.m.s);
break;
default:
if (left_dragging_) {
mouse_type = kCGEventLeftMouseDragged;
mouse_button = kCGMouseButtonLeft;
} else if (right_dragging_) {
mouse_type = kCGEventRightMouseDragged;
mouse_button = kCGMouseButtonRight;
} else {
mouse_type = kCGEventMouseMoved;
mouse_button = kCGMouseButtonLeft;
}
mouse_event =
CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, mouse_button);
break;
}
if (mouse_event) {
CGEventPost(kCGHIDEventTap, mouse_event);
CFRelease(mouse_event);
} }
return 0; return 0;
@@ -7,6 +7,7 @@
#ifndef _MOUSE_CONTROLLER_H_ #ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_ #define _MOUSE_CONTROLLER_H_
#include <chrono>
#include <vector> #include <vector>
#include "device_controller.h" #include "device_controller.h"
@@ -24,9 +25,24 @@ class MouseController : public DeviceController {
virtual int SendMouseCommand(RemoteAction remote_action, int display_index); virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
private: 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_; std::vector<DisplayInfo> display_info_list_;
bool left_dragging_ = false; bool left_dragging_ = false;
bool right_dragging_ = false; bool right_dragging_ = false;
ClickTracker left_click_tracker_;
ClickTracker right_click_tracker_;
ClickTracker middle_click_tracker_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif
+228
View File
@@ -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_
+295
View File
@@ -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
+112
View File
@@ -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_
+320
View File
@@ -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
+415
View File
@@ -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
+687
View File
@@ -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
+236
View File
@@ -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
-2
View File
@@ -7,8 +7,6 @@
#ifndef _LAYOUT_STYLE_H_ #ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_ #define _LAYOUT_STYLE_H_
#include "render.h"
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_ #define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_ #define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_ #define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
-2
View File
@@ -7,8 +7,6 @@
#ifndef _LAYOUT_STYLE_H_ #ifndef _LAYOUT_STYLE_H_
#define _LAYOUT_STYLE_H_ #define _LAYOUT_STYLE_H_
#include "render.h"
#define TITLE_BAR_HEIGHT 0.0625f #define TITLE_BAR_HEIGHT 0.0625f
#define TITLE_BAR_BUTTON_WIDTH 0.0625f #define TITLE_BAR_BUTTON_WIDTH 0.0625f
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f #define TITLE_BAR_BUTTON_HEIGHT 0.0625f
+212 -170
View File
@@ -19,176 +19,218 @@ struct TranslationRow {
}; };
// Single source of truth for all UI strings. // Single source of truth for all UI strings.
#define CROSSDESK_LOCALIZATION_ALL(X) \ #define CROSSDESK_LOCALIZATION_ALL(X) \
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \ X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \ X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
u8"Скопировано в буфер обмена") \ u8"Скопировано в буфер обмена") \
X(password, u8"密码", "Password", u8"Пароль") \ X(password, u8"密码", "Password", u8"Пароль") \
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
X(remote_desktop, u8"远程桌面", "Remote Desktop", \ X(remote_desktop, u8"远程桌面", "Remote Desktop", \
u8"Удаленный рабочий стол") \ u8"Удаленный рабочий стол") \
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \ X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
X(connect, u8"连接", "Connect", u8"Подключиться") \ X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
X(recent_connections, u8"近期连接", "Recent Connections", \ X(connect, u8"连接", "Connect", u8"Подключиться") \
u8"Недавние подключения") \ X(recent_connections, u8"近期连接", "Recent Connections", \
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \ u8"Недавние подключения") \
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \ X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \ X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
u8"Развернуть панель управления") \ X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \ u8"Развернуть панель управления") \
u8"Свернуть панель управления") \ X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \ u8"Свернуть панель управления") \
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \ X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
u8"Показать статистику трафика") \ X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \ u8"Показать статистику трафика") \
u8"Скрыть статистику трафика") \ X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
X(video, u8"视频", "Video", u8"Видео") \ u8"Скрыть статистику трафика") \
X(audio, u8"频", "Audio", u8"Аудио") \ X(video, u8"频", "Video", u8"Видео") \
X(data, u8"数据", "Data", u8"Данные") \ X(audio, u8"音频", "Audio", u8"Аудио") \
X(total, u8"总计", "Total", u8"Итого") \ X(data, u8"数据", "Data", u8"Данные") \
X(in, u8"输入", "In", u8"Вход") \ X(total, u8"总计", "Total", u8"Итого") \
X(out, u8"输", "Out", u8"Выход") \ X(in, u8"输", "In", u8"Вход") \
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \ X(out, u8"输出", "Out", u8"Выход") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \ X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
u8"Выйти из полноэкранного режима") \ X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \ u8"Выйти из полноэкранного режима") \
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \ X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \ X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
X(mute, u8"音", " Mute", u8"Без звука") \ X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \ X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \ X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \ X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(remote_password_box_visible, u8"远端密码框已出现", \ X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
"Remote password box visible", u8"Окно ввода пароля видно") \ X(remote_password_box_visible, u8"远端密码框已出现", \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \ "Remote password box visible", u8"Окно ввода пароля видно") \
"Remote lock screen visible, send SAS", \ X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
u8"Видна блокировка, отправьте SAS") \ "Remote lock screen visible, send SAS", \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ u8"Видна блокировка, отправьте SAS") \
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \ X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
X(remote_service_unavailable, u8"远端Windows服务不可用", \ "Remote secure desktop active", u8"Активен защищенный рабочий стол") \
"Remote Windows service unavailable", \ X(remote_service_unavailable, u8"远端Windows服务不可用", \
u8"Служба Windows на удаленной стороне недоступна") \ "Remote Windows service unavailable", \
X(windows_service_setup_title, u8"安装 CrossDesk Service", \ u8"Служба Windows на удаленной стороне недоступна") \
"Install CrossDesk Service", u8"Установить CrossDesk Service") \ X(windows_service_setup_title, u8"安装 CrossDesk Service", \
X(windows_service_setup_message, \ "Install CrossDesk Service", u8"Установить CrossDesk Service") \
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \ X(windows_service_setup_message, \
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \ u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \ u8"Service。\n未安装该服务不影响 CrossDesk " \
X(install_windows_service, u8"安装", "Install", \ u8"正常使用,仅无法在锁屏状态下控制本机。", \
u8"Установить службу") \ "To support remote control of this device while it is locked, CrossDesk " \
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \ "Service must be installed with administrator permission.\nWithout this " \
u8"Установка службы...") \ "service, CrossDesk still works normally; only lock-screen control of " \
X(windows_service_install_success, u8"服务已安装并启动", \ "this computer is unavailable.", \
"Service installed and started", u8"Служба установлена и запущена") \ u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \ u8"блокировки, необходимо установить CrossDesk Service с правами " \
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \ u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \ u8"нормально; будет недоступно только управление этим компьютером на " \
X(remote_unlock_requires_secure_desktop, \ u8"экране блокировки.") \
u8"当前仍需要安全桌面专用采集/输入", \ X(install_windows_service, u8"安装", "Install", u8"Установить") \
"Secure desktop capture/input is still required", \ X(windows_service_settings_label, u8"锁屏控制服务:", \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \ "Lock Screen Service:", u8"Служба блокировки экрана:") \
u8"стола") \ X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
X(settings, u8"设置", "Settings", u8"Настройки") \ X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
X(language, u8"语言:", "Language:", u8"Язык:") \ u8"Больше не напоминать") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ X(windows_service_prompt_suppressed_message, \
X(video_frame_rate, u8"画面采集帧率:", \ u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
"Video Capture Frame Rate:", u8"Частота захвата видео:") \ "You will not be reminded again. To enable remote control while locked " \
X(video_quality_high, u8"高", "High", u8"Высокое") \ "later, click Install in Settings.", \
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \ u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
X(video_quality_low, u8"低", "Low", u8"Низкое") \ u8"экране блокировки, нажмите «Установить» в настройках.") \
X(video_encode_format, u8"视频编码格式:", \ X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
"Video Encode Format:", u8"Формат кодека видео:") \ u8"Установка службы...") \
X(av1, u8"AV1", "AV1", "AV1") \ X(windows_service_install_success, u8"服务已安装并启动", \
X(h264, u8"H.264", "H.264", "H.264") \ "Service installed and started", u8"Служба установлена и запущена") \
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \ X(windows_service_install_failed, \
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \ u8"服务安装失败。请确认 " \
X(enable_turn, u8"启用中继服务:", \ u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
"Enable TURN Service:", u8"Включить TURN-сервис:") \ u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \ "Service installation failed. Make sure CrossDesk.exe, " \
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \ "crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
u8"Конфигурация self-hosted") \ "portable folder, then approve the administrator prompt.", \
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \ u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
u8"Настройки self-hosted") \ u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
X(self_hosted_server_address, u8"服务器地址:", \ u8"одной папке портативной версии, затем подтвердите запрос прав " \
"Server Address:", u8"Адрес сервера:") \ u8"администратора.") \
X(self_hosted_server_port, u8"信令服务端口:", \ X(remote_unlock_requires_secure_desktop, \
"Signal Service Port:", u8"Порт сигнального сервиса:") \ u8"当前仍需要安全桌面专用采集/输入", \
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \ "Secure desktop capture/input is still required", \
"Relay Service Port:", u8"Порт реле-сервиса:") \ u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
X(ok, u8"确认", "OK", u8"ОК") \ u8"стола") \
X(cancel, u8"取消", "Cancel", u8"Отмена") \ X(settings, u8"设置", "Settings", u8"Настройки") \
X(new_password, u8"请输入六位密码:", \ X(language, u8"语言:", "Language:", u8"Язык:") \
"Please input a six-char password:", u8"Введите шестизначный пароль:") \ X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
X(input_password, u8"请输入密码:", \ X(video_frame_rate, u8"画面采集帧率:", \
"Please input password:", u8"Введите пароль:") \ "Video Capture Frame Rate:", u8"Частота захвата видео:") \
X(validate_password, u8"验证密码中...", "Validate password ...", \ X(video_quality_high, u8"高", "High", u8"Высокое") \
u8"Проверка пароля...") \ X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
X(reinput_password, u8"请重新输入密码", "Please input password again", \ X(video_quality_low, u8"低", "Low", u8"Низкое") \
u8"Повторно введите пароль") \ X(video_encode_format, u8"视频编码格式:", \
X(remember_password, u8"记住密码", "Remember password", \ "Video Encode Format:", u8"Формат кодека видео:") \
u8"Запомнить пароль") \ X(av1, u8"AV1", "AV1", "AV1") \
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \ X(h264, u8"H.264", "H.264", "H.264") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \ X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
u8"Нет подключения к серверу") \ "Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ X(enable_turn, u8"启用中继服务:", \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ "Enable TURN Service:", u8"Включить TURN-сервис:") \
u8"P2P отключено") \ X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \ X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
u8"Подключение P2P...") \ u8"Конфигурация self-hosted") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
u8"Получение изображения...") \ u8"Настройки self-hosted") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ X(self_hosted_server_address, u8"服务器地址:", \
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \ "Server Address:", u8"Адрес сервера:") \
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \ X(self_hosted_server_port, u8"信令服务端口:", \
X(about, u8"关于", "About", u8"О программе") \ "Signal Service Port:", u8"Порт сигнального сервиса:") \
X(notification, u8"通知", "Notification", u8"Уведомление") \ X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
X(new_version_available, u8"新版本可用", "New Version Available", \ "Relay Service Port:", u8"Порт реле-сервиса:") \
u8"Доступна новая версия") \ X(ok, u8"确认", "OK", u8"ОК") \
X(version, u8"版本", "Version", u8"Версия") \ X(cancel, u8"取消", "Cancel", u8"Отмена") \
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ X(new_password, u8"请输入六位密码:", \
X(access_website, u8"访问官网: ", \ "Please input a six-char password:", u8"Введите шестизначный пароль:") \
"Access Website: ", u8"Официальный сайт: ") \ X(input_password, u8"请输入密码:", \
X(update, u8"更新", "Update", u8"Обновить") \ "Please input password:", u8"Введите пароль:") \
X(confirm_delete_connection, u8"确认删除此连接", \ X(validate_password, u8"验证密码中...", "Validate password ...", \
"Confirm to delete this connection", u8"Удалить это подключение?") \ u8"Проверка пароля...") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ X(reinput_password, u8"请重新输入密码", "Please input password again", \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ u8"Повторно введите пароль") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ X(remember_password, u8"记住密码", "Remember password", \
u8"Вступит в силу после перезапуска") \ u8"Запомнить пароль") \
X(select_file, u8"选择文件发送", "Select File to Send", \ X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
u8"Выбрать файл для отправки") \ X(signal_disconnected, u8"未连接服务器", "Disconnected", \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \ u8"Нет подключения к серверу") \
u8"Прогресс передачи файлов") \ X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
X(queued, u8"队列中", "Queued", u8"В очереди") \ "Certificate verification failed. Reinstall the self-hosted root " \
X(sending, u8"正在传输", "Sending", u8"Передача") \ "certificate.", \
X(completed, u8"已完成", "Completed", u8"Завершено") \ u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
X(failed, u8"失败", "Failed", u8"Ошибка") \ X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \ X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \ u8"P2P отключено") \
X(connection_status, u8"连接状态:", \ X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
"Connection Status:", u8"Состояние соединения:") \ u8"Подключение P2P...") \
X(file_transfer_save_path, u8"文件接收保存路径:", \ X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \ u8"Сбор кандидатов...") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \ X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \ u8"Получение изображения...") \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \ X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \ X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \ X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \ X(about, u8"关于", "About", u8"О программе") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \ X(notification, u8"通知", "Notification", u8"Уведомление") \
X(online, u8"在线", "Online", u8"Онлайн") \ X(new_version_available, u8"新版本可用", "New Version Available", \
X(offline, u8"离线", "Offline", u8"Офлайн") \ u8"Доступна новая версия") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \ X(version, u8"版本", "Version", u8"Версия") \
X(request_permissions, u8"权限请求", "Request Permissions", \ X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
u8"Запрос разрешений") \ X(access_website, u8"访问官网: ", \
X(screen_recording_permission, u8"屏幕录制权限", \ "Access Website: ", u8"Официальный сайт: ") \
"Screen Recording Permission", u8"Разрешение на запись экрана") \ X(update, u8"更新", "Update", u8"Обновить") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \ X(connection_alias, u8"修改名称", "Edit Alias", \
u8"Разрешение специальных возможностей") \ u8"Изменить имя подключения") \
X(permission_required_message, u8"该应用需要授权以下权限:", \ X(delete_connection, u8"删除连接", "Delete Connection", \
"The application requires the following permissions:", \ u8"Удалить подключение") \
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"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \
X(select_file, u8"选择文件发送", "Select File to Send", \
u8"Выбрать файл для отправки") \
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \
X(sending, u8"正在传输", "Sending", u8"Передача") \
X(completed, u8"已完成", "Completed", u8"Завершено") \
X(failed, u8"失败", "Failed", u8"Ошибка") \
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
X(connection_status, u8"连接状态:", \
"Connection Status:", u8"Состояние соединения:") \
X(file_transfer_save_path, u8"文件接收保存路径:", \
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
X(online, u8"在线", "Online", u8"Онлайн") \
X(offline, u8"离线", "Offline", u8"Офлайн") \
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
X(request_permissions, u8"权限请求", "Request Permissions", \
u8"Запрос разрешений") \
X(screen_recording_permission, u8"屏幕录制权限", \
"Screen Recording Permission", u8"Разрешение на запись экрана") \
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
u8"Разрешение специальных возможностей") \
X(permission_required_message, u8"该应用需要授权以下权限:", \
"The application requires the following permissions:", \
u8"Для работы приложения требуются следующие разрешения:") \
X(exit_program, u8"退出", "Exit", u8"Выход") X(exit_program, u8"退出", "Exit", u8"Выход")
inline constexpr TranslationRow kTranslationRows[] = { inline constexpr TranslationRow kTranslationRows[] = {
@@ -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_
-363
View File
@@ -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
+40
View File
@@ -0,0 +1,40 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LINUX_TRAY_H_
#define _LINUX_TRAY_H_
#if defined(__linux__) && !defined(__APPLE__)
#include <cstdint>
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct LinuxTrayImpl;
class LinuxTray {
public:
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
~LinuxTray();
bool MinimizeToTray();
void RemoveTrayIcon();
void ProcessEvents();
private:
std::unique_ptr<LinuxTrayImpl> impl_;
};
} // namespace crossdesk
#endif // defined(__linux__) && !defined(__APPLE__)
#endif // _LINUX_TRAY_H_
+34
View File
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct MacTrayImpl;
class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
~MacTray();
void MinimizeToTray();
void RemoveTrayIcon();
private:
std::unique_ptr<MacTrayImpl> impl_;
};
} // namespace crossdesk
#endif // _MAC_TRAY_H_
+249
View File
@@ -0,0 +1,249 @@
#include "mac_tray.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
#include "localization.h"
#include <utility>
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)exitApplication:(id)sender;
@end
namespace crossdesk {
struct MacTrayImpl {
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
int language_index_value)
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
~MacTrayImpl() {
RemoveTrayIcon();
target = nil;
}
void MinimizeToTray() {
EnsureStatusItem();
if (app_window) {
SDL_HideWindow(app_window);
}
}
void RemoveTrayIcon() {
if (!status_item) {
return;
}
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
status_item = nil;
}
void ShowWindow() {
if (!app_window) {
return;
}
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
void ShowMenu() {
EnsureStatusItem();
if (!status_item) {
return;
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
action:@selector(exitApplication:)
keyEquivalent:@""];
[exit_item setTarget:target];
[menu addItem:exit_item];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
const NSRect bounds = [button bounds];
[menu popUpMenuPositioningItem:nil
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
inView:button];
}
void RequestExit() {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
private:
void EnsureStatusItem() {
if (status_item) {
return;
}
status_item = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSSquareStatusItemLength];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
[button setToolTip:NSStringFromUtf8(tooltip)];
NSImage *crossdesk_icon = LoadCrossDeskIcon();
if (crossdesk_icon) {
NSImage *status_icon = [crossdesk_icon copy];
[status_icon setSize:NSMakeSize(18.0, 18.0)];
[status_icon setTemplate:NO];
[button setImage:status_icon];
[button setImagePosition:NSImageOnly];
} else {
[button setTitle:@"CD"];
}
[button setTarget:target];
[button setAction:@selector(statusItemClicked:)];
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
}
NSString *NSStringFromUtf8(const std::string &text) {
return [NSString stringWithUTF8String:text.c_str()];
}
NSImage *LoadCrossDeskIcon() {
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
if (!icon) {
icon = LoadIconFromBundleResource(@"crossedesk");
}
if (!icon) {
icon = LoadIconFromDevelopmentPath();
}
if (!icon) {
icon = [NSApp applicationIconImage];
}
return icon;
}
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
NSString *icon_path =
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
return LoadIconFromPath(icon_path);
}
NSImage *LoadIconFromDevelopmentPath() {
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
NSString *current_directory =
[[NSFileManager defaultManager] currentDirectoryPath];
[candidate_paths
addObject:[current_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
const char *base_path = SDL_GetBasePath();
if (base_path && base_path[0] != '\0') {
NSString *base_directory = NSStringFromUtf8(base_path);
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"../../../../icons/macos/crossdesk.icns"]];
}
for (NSString *candidate_path in candidate_paths) {
NSImage *icon = LoadIconFromPath(
[candidate_path stringByStandardizingPath]);
if (icon) {
return icon;
}
}
return nil;
}
NSImage *LoadIconFromPath(NSString *icon_path) {
if (![icon_path length]) {
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
return nil;
}
return [[NSImage alloc] initWithContentsOfFile:icon_path];
}
::SDL_Window *app_window = nullptr;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
CrossDeskMacTrayTarget *target = nil;
};
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
int language_index)
: impl_(
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
} // namespace crossdesk
@implementation CrossDeskMacTrayTarget {
crossdesk::MacTrayImpl *owner_;
}
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)statusItemClicked:(id)sender {
(void)sender;
if (!owner_) {
return;
}
NSEvent *event = [NSApp currentEvent];
if (event && [event type] == NSEventTypeRightMouseUp) {
owner_->ShowMenu();
return;
}
owner_->ShowWindow();
}
- (void)exitApplication:(id)sender {
(void)sender;
if (owner_) {
owner_->RequestExit();
}
}
@end
#endif // __APPLE__
+6 -3023
View File
File diff suppressed because it is too large Load Diff
+18 -762
View File
@@ -1,772 +1,28 @@
/* #ifndef CROSSDESK_GUI_RENDER_H_
* @Author: DI JUNKUN #define CROSSDESK_GUI_RENDER_H_
* @Date: 2024-05-29
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAIN_WINDOW_H_ #include <memory>
#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
namespace crossdesk { namespace crossdesk {
class GuiApplication;
// Stable application-facing facade. The SDL application and feature
// controllers remain private implementation details.
class Render { class Render {
public: 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();
~Render(); ~Render();
public: Render(const Render &) = delete;
Render &operator=(const Render &) = delete;
int Run(); int Run();
private: private:
void InitializeLogger(); std::unique_ptr<GuiApplication> application_;
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);
#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,
};
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_;
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;
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_;
SDL_AudioDeviceID output_dev_;
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_;
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;
#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 } // namespace crossdesk
#endif // CROSSDESK_GUI_RENDER_H_
File diff suppressed because it is too large Load Diff
+439
View File
@@ -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_, &params_, 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,23 +4,26 @@
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/ */
#ifndef _DEVICE_PRESENCE_H_ #ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#define _DEVICE_PRESENCE_H_ #define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
class DevicePresence { namespace crossdesk {
public:
void SetOnline(const std::string& device_id, bool online) { class DevicePresenceCache {
public:
void SetOnline(const std::string &device_id, bool online) {
std::lock_guard<std::mutex> lock(mutex_); std::lock_guard<std::mutex> lock(mutex_);
cache_[device_id] = online; cache_[device_id] = online;
} }
bool IsOnline(const std::string& device_id) const { bool IsOnline(const std::string &device_id) const {
std::lock_guard<std::mutex> lock(mutex_); 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() { void Clear() {
@@ -28,9 +31,11 @@ class DevicePresence {
cache_.clear(); cache_.clear();
} }
private: private:
std::unordered_map<std::string, bool> cache_; std::unordered_map<std::string, bool> cache_;
mutable std::mutex mutex_; mutable std::mutex mutex_;
}; };
#endif } // namespace crossdesk
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
+246
View File
@@ -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(&params_);
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
+96
View File
@@ -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_
+21
View File
@@ -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_
+113
View File
@@ -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
+267
View File
@@ -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
+524
View File
@@ -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
+47
View File
@@ -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_
+111
View File
@@ -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
+117
View File
@@ -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
+17
View File
@@ -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_
+175
View File
@@ -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_
+186
View File
@@ -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_
+262
View File
@@ -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
-48
View File
@@ -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,14 +1,14 @@
#include <random> #include <random>
#include "application/gui_application.h"
#include "layout_relative.h" #include "layout_relative.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
int Render::LocalWindow() { int GuiApplication::LocalWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
float local_window_width = io.DisplaySize.x * 0.5f; float local_window_width = io.DisplaySize.x * 0.5f;
float local_window_height = float local_window_height =
io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT); io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT);
@@ -239,7 +239,7 @@ int Render::LocalWindow() {
if (config_center_->IsSelfHosted()) { if (config_center_->IsSelfHosted()) {
std::string self_hosted_id_str; std::string self_hosted_id_str;
if (strlen(self_hosted_id_) > 0) { if (strlen(self_hosted_id_) > 0) {
const char* at_pos = strchr(self_hosted_id_, '@'); const char *at_pos = strchr(self_hosted_id_, '@');
if (at_pos != nullptr) { if (at_pos != nullptr) {
self_hosted_id_str = self_hosted_id_str =
std::string(self_hosted_id_, at_pos - self_hosted_id_); std::string(self_hosted_id_, at_pos - self_hosted_id_);
@@ -266,7 +266,7 @@ int Render::LocalWindow() {
'\0'; '\0';
} }
SaveSettingsIntoCacheFile(); settings_.Save();
memset(new_password_, 0, sizeof(new_password_)); memset(new_password_, 0, sizeof(new_password_));
@@ -300,4 +300,4 @@ int Render::LocalWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -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,14 +1,14 @@
#include "application/gui_application.h"
#include "layout_relative.h" #include "layout_relative.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
static int InputTextCallback(ImGuiInputTextCallbackData* data); static int InputTextCallback(ImGuiInputTextCallbackData *data);
int Render::RemoteWindow() { int GuiApplication::RemoteWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
float remote_window_width = io.DisplaySize.x * 0.5f; float remote_window_width = io.DisplaySize.x * 0.5f;
float remote_window_height = float remote_window_height =
io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT); io.DisplaySize.y * (1 - TITLE_BAR_HEIGHT - STATUS_BAR_HEIGHT);
@@ -92,16 +92,16 @@ int Render::RemoteWindow() {
bool should_connect = false; bool should_connect = false;
bool already_connected = false; bool already_connected = false;
for (auto& [id, props] : recent_connections_) { for (auto &[id, props] : recent_connections_) {
if (id.find(remote_id) != std::string::npos) { if (id.find(remote_id) != std::string::npos) {
found = true; found = true;
target_remote_id = props.remote_id; target_remote_id = props.remote_id;
target_password = props.password; target_password = props.password;
{ {
// std::shared_lock lock(client_properties_mutex_); // std::shared_lock lock(remote_sessions_mutex_);
if (client_properties_.find(remote_id) != if (remote_sessions_.find(remote_id) !=
client_properties_.end()) { remote_sessions_.end()) {
if (!client_properties_[remote_id]->connection_established_) { if (!remote_sessions_[remote_id]->connection_established_) {
should_connect = true; should_connect = true;
} else { } else {
already_connected = true; already_connected = true;
@@ -134,8 +134,8 @@ int Render::RemoteWindow() {
if (elapsed >= 1000) { if (elapsed >= 1000) {
last_rejoin_check_time_ = now; last_rejoin_check_time_ = now;
need_to_rejoin_ = false; need_to_rejoin_ = false;
// std::shared_lock lock(client_properties_mutex_); // std::shared_lock lock(remote_sessions_mutex_);
for (const auto& [_, props] : client_properties_) { for (const auto &[_, props] : remote_sessions_) {
if (props->rejoin_) { if (props->rejoin_) {
ConnectTo(props->remote_id_, props->remote_password_, ConnectTo(props->remote_id_, props->remote_password_,
props->remember_password_); props->remember_password_);
@@ -152,7 +152,7 @@ int Render::RemoteWindow() {
return 0; return 0;
} }
static int InputTextCallback(ImGuiInputTextCallbackData* data) { static int InputTextCallback(ImGuiInputTextCallbackData *data) {
if (data->BufTextLen > 3 && data->Buf[3] != ' ') { if (data->BufTextLen > 3 && data->Buf[3] != ' ') {
data->InsertChars(3, " "); data->InsertChars(3, " ");
} }
@@ -164,112 +164,4 @@ static int InputTextCallback(ImGuiInputTextCallbackData* data) {
return 0; return 0;
} }
int Render::ConnectTo(const std::string& remote_id, const char* password, } // namespace crossdesk
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_, &params_, 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,18 +6,18 @@
#include <thread> #include <thread>
#include <vector> #include <vector>
#include "application/gui_application.h"
#include "file_transfer.h" #include "file_transfer.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
#include "tinyfiledialogs.h" #include "tinyfiledialogs.h"
namespace crossdesk { namespace crossdesk {
namespace { namespace {
void ShowControlBarTooltip(const std::string& text) { void ShowControlBarTooltip(const std::string &text) {
if (!ImGui::IsItemHovered() || text.empty()) { if (!ImGui::IsItemHovered() || text.empty()) {
return; return;
} }
@@ -29,10 +29,11 @@ void ShowControlBarTooltip(const std::string& text) {
ImGui::EndTooltip(); ImGui::EndTooltip();
} }
} // namespace } // namespace
int CountDigits(int number) { int CountDigits(int number) {
if (number == 0) return 1; if (number == 0)
return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1; return (int)std::floor(std::log10(std::abs(number))) + 1;
} }
@@ -57,98 +58,20 @@ int LossRateDisplay(float loss_rate) {
return 0; return 0;
} }
std::string Render::OpenFileDialog(std::string title) { std::string GuiApplication::OpenFileDialog(std::string title) {
const char* path = tinyfd_openFileDialog(title.c_str(), const char *path = tinyfd_openFileDialog(title.c_str(),
"", // default path "", // default path
0, // number of filters 0, // number of filters
nullptr, // filters nullptr, // filters
nullptr, // filter description nullptr, // filter description
0 // no multiple selection 0 // no multiple selection
); );
return path ? path : ""; return path ? path : "";
} }
void Render::ProcessSelectedFile( int GuiApplication::ControlBar(
const std::string& path, std::shared_ptr<RemoteSession> &props) {
const std::shared_ptr<SubStreamWindowProperties>& props,
const std::string& file_label, const std::string& remote_id) {
if (path.empty()) {
return;
}
FileTransferState* file_transfer_state =
props ? &props->file_transfer_ : &file_transfer_;
LOG_INFO("Selected file: {}", path.c_str());
std::filesystem::path file_path = std::filesystem::u8path(path);
// Get file size
std::error_code ec;
uint64_t file_size = std::filesystem::file_size(file_path, ec);
if (ec) {
LOG_ERROR("Failed to get file size: {}", ec.message().c_str());
file_size = 0;
}
// Add file to transfer list
{
std::lock_guard<std::mutex> lock(
file_transfer_state->file_transfer_list_mutex_);
FileTransferState::FileTransferInfo info;
info.file_name = file_path.filename().u8string();
info.file_path = file_path; // Store full path for precise matching
info.file_size = file_size;
info.status = FileTransferState::FileTransferStatus::Queued;
info.sent_bytes = 0;
info.file_id = 0;
info.rate_bps = 0;
file_transfer_state->file_transfer_list_.push_back(info);
}
file_transfer_state->file_transfer_window_visible_ = true;
if (file_transfer_state->file_sending_.load()) {
// Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
queued_file.remote_id = remote_id;
file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
}
LOG_INFO("File added to queue: {} ({} files in queue)",
file_path.filename().string().c_str(), queue_size);
} else {
StartFileTransfer(props, file_path, file_label, remote_id);
if (file_transfer_state->file_sending_.load()) {
} else {
// Failed to start (race condition: another file started between
// check and call) Add to queue
size_t queue_size = 0;
{
std::lock_guard<std::mutex> lock(
file_transfer_state->file_queue_mutex_);
FileTransferState::QueuedFile queued_file;
queued_file.file_path = file_path;
queued_file.file_label = file_label;
queued_file.remote_id = remote_id;
file_transfer_state->file_send_queue_.push(queued_file);
queue_size = file_transfer_state->file_send_queue_.size();
}
LOG_INFO(
"File added to queue after race condition: {} ({} files in "
"queue)",
file_path.filename().string().c_str(), queue_size);
}
}
}
int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
float button_width = title_bar_height_ * 0.8f; float button_width = title_bar_height_ * 0.8f;
float button_height = title_bar_height_ * 0.8f; float button_height = title_bar_height_ * 0.8f;
float line_padding = title_bar_height_ * 0.12f; float line_padding = title_bar_height_ * 0.12f;
@@ -160,7 +83,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
? props->control_window_width_ * 0.03f ? props->control_window_width_ * 0.03f
: props->control_window_width_ * 0.17f); : props->control_window_width_ * 0.17f);
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList *draw_list = ImGui::GetWindowDrawList();
if (!props->is_control_bar_in_left_) { if (!props->is_control_bar_in_left_) {
draw_list->AddLine( draw_list->AddLine(
ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f, ImVec2(ImGui::GetCursorScreenPos().x - button_height * 0.5f,
@@ -191,7 +114,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
RemoteAction remote_action; RemoteAction remote_action;
remote_action.type = ControlType::display_id; remote_action.type = ControlType::display_id;
remote_action.d = i; remote_action.d = i;
if (props->connection_status_ == ConnectionStatus::Connected) { if (props->connection_status_.load() == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(), SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str()); props->control_data_label_.c_str());
@@ -214,8 +137,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::to_string(props->selected_display_ + 1).c_str()); std::to_string(props->selected_display_ + 1).c_str());
auto send_service_command = [&](ServiceCommandFlag flag, auto send_service_command = [&](ServiceCommandFlag flag,
const char* log_action) { const char *log_action) {
if (props->connection_status_ == ConnectionStatus::Connected && if (props->connection_status_.load() == ConnectionStatus::Connected &&
props->peer_) { props->peer_) {
RemoteAction remote_action; RemoteAction remote_action;
remote_action.type = ControlType::service_command; remote_action.type = ControlType::service_command;
@@ -290,9 +213,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
draw_list->AddLine( draw_list->AddLine(
ImVec2(disable_mouse_x - line_thickness * 0.7f, ImVec2(disable_mouse_x - line_thickness * 0.7f,
disable_mouse_y + line_thickness * 0.7f), disable_mouse_y + line_thickness * 0.7f),
ImVec2( ImVec2(mouse_x + button_width - line_padding - line_thickness * 0.7f,
mouse_x + button_width - line_padding - line_thickness * 0.7f, mouse_y + button_height - line_padding +
mouse_y + button_height - line_padding + line_thickness * 0.7f), line_thickness * 0.7f),
mouse_button_hovered ? IM_COL32(66, 150, 250, 255) mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255), : IM_COL32(179, 213, 253, 255),
line_thickness); line_thickness);
@@ -341,9 +264,9 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
draw_list->AddLine( draw_list->AddLine(
ImVec2(disable_audio_x - line_thickness * 0.7f, ImVec2(disable_audio_x - line_thickness * 0.7f,
disable_audio_y + line_thickness * 0.7f), disable_audio_y + line_thickness * 0.7f),
ImVec2( ImVec2(audio_x + button_width - line_padding - line_thickness * 0.7f,
audio_x + button_width - line_padding - line_thickness * 0.7f, audio_y + button_height - line_padding +
audio_y + button_height - line_padding + line_thickness * 0.7f), line_thickness * 0.7f),
audio_button_hovered ? IM_COL32(66, 150, 250, 255) audio_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255), : IM_COL32(179, 213, 253, 255),
line_thickness); line_thickness);
@@ -356,7 +279,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::string title = std::string title =
localization::select_file[localization_language_index_]; localization::select_file[localization_language_index_];
std::string path = OpenFileDialog(title); std::string path = OpenFileDialog(title);
ProcessSelectedFile(path, props, file_label_); transfers_.ProcessSelectedFile(path, props, file_label_);
} }
ShowControlBarTooltip( ShowControlBarTooltip(
localization::select_file[localization_language_index_]); localization::select_file[localization_language_index_]);
@@ -428,7 +351,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
if (ImGui::Button(close_button.c_str(), if (ImGui::Button(close_button.c_str(),
ImVec2(button_width, button_height))) { ImVec2(button_width, button_height))) {
CleanupPeer(props); CloseRemoteSession(props);
} }
ShowControlBarTooltip( ShowControlBarTooltip(
localization::disconnect[localization_language_index_]); localization::disconnect[localization_language_index_]);
@@ -487,7 +410,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
return 0; 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, ImGui::SetCursorPos(ImVec2(props->control_window_width_ * 0.048f,
props->control_window_min_height_)); props->control_window_min_height_));
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
@@ -582,4 +506,4 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
+56
View File
@@ -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 "layout_relative.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h" #include "rounded_corner_button.h"
constexpr double kNewVersionIconBlinkIntervalSec = 2.0; constexpr double kNewVersionIconBlinkIntervalSec = 2.0;
@@ -9,8 +9,8 @@ constexpr double kNewVersionIconBlinkOnTimeSec = 1.0;
namespace crossdesk { namespace crossdesk {
int Render::TitleBar(bool main_window) { int GuiApplication::TitleBar(bool main_window) {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
float title_bar_width = title_bar_width_; float title_bar_width = title_bar_width_;
float title_bar_height = title_bar_height_; float title_bar_height = title_bar_height_;
float title_bar_height_padding = title_bar_height_; float title_bar_height_padding = title_bar_height_;
@@ -58,7 +58,7 @@ int Render::TitleBar(bool main_window) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
// get draw list // get draw list
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImGui::SetCursorPos( ImGui::SetCursorPos(
ImVec2(title_bar_width - title_bar_button_width * 3, 0.0f)); ImVec2(title_bar_width - title_bar_button_width * 3, 0.0f));
@@ -83,7 +83,7 @@ int Render::TitleBar(bool main_window) {
bar_pos_y + title_bar_button_height * 0.15f), bar_pos_y + title_bar_button_height * 0.15f),
IM_COL32(0, 0, 0, 255)); IM_COL32(0, 0, 0, 255));
std::string title_bar_menu_button = "##title_bar_menu"; // ICON_FA_BARS; std::string title_bar_menu_button = "##title_bar_menu"; // ICON_FA_BARS;
std::string title_bar_menu = "##title_bar_menu"; std::string title_bar_menu = "##title_bar_menu";
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
@@ -195,7 +195,7 @@ int Render::TitleBar(bool main_window) {
title_bar_button_width * (main_window ? 2 : 3) + title_bar_button_width * (main_window ? 2 : 3) +
title_bar_button_width * 0.33f; title_bar_button_width * 0.33f;
float minimize_pos_y = title_bar_button_height * 0.5f; float minimize_pos_y = title_bar_button_height * 0.5f;
std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS; std::string window_minimize_button = "##minimize"; // ICON_FA_MINUS;
if (ImGui::Button(window_minimize_button.c_str(), if (ImGui::Button(window_minimize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) { ImVec2(title_bar_button_width, title_bar_button_height))) {
if (main_window) { if (main_window) {
@@ -225,7 +225,7 @@ int Render::TitleBar(bool main_window) {
float pos_x_bottom = title_bar_width - title_bar_button_width * 1.6f; float pos_x_bottom = title_bar_width - title_bar_button_width * 1.6f;
float pos_y_bottom = title_bar_button_height * 0.28f; float pos_y_bottom = title_bar_button_height * 0.28f;
std::string window_restore_button = std::string window_restore_button =
"##restore"; // ICON_FA_WINDOW_RESTORE; "##restore"; // ICON_FA_WINDOW_RESTORE;
if (ImGui::Button( if (ImGui::Button(
window_restore_button.c_str(), window_restore_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) { ImVec2(title_bar_button_width, title_bar_button_height))) {
@@ -259,8 +259,7 @@ int Render::TitleBar(bool main_window) {
float maximize_pos_x = title_bar_width - title_bar_button_width * 1.5f - float maximize_pos_x = title_bar_width - title_bar_button_width * 1.5f -
title_bar_button_height * 0.165f; title_bar_button_height * 0.165f;
float maximize_pos_y = title_bar_button_height * 0.33f; float maximize_pos_y = title_bar_button_height * 0.33f;
std::string window_maximize_button = std::string window_maximize_button = "##maximize"; // ICON_FA_SQUARE_FULL;
"##maximize"; // ICON_FA_SQUARE_FULL;
if (ImGui::Button( if (ImGui::Button(
window_maximize_button.c_str(), window_maximize_button.c_str(),
ImVec2(title_bar_button_width, title_bar_button_height))) { ImVec2(title_bar_button_width, title_bar_button_height))) {
@@ -285,7 +284,7 @@ int Render::TitleBar(bool main_window) {
float xmark_pos_x = xmark_button_pos_x + title_bar_button_width * 0.5f; float xmark_pos_x = xmark_button_pos_x + title_bar_button_width * 0.5f;
float xmark_pos_y = title_bar_button_height * 0.5f; float xmark_pos_y = title_bar_button_height * 0.5f;
float xmark_size = title_bar_button_width * 0.33f; float xmark_size = title_bar_button_width * 0.33f;
std::string close_button = "##xmark"; // ICON_FA_XMARK; std::string close_button = "##xmark"; // ICON_FA_XMARK;
bool close_button_clicked = false; bool close_button_clicked = false;
if (main_window) { if (main_window) {
close_button_clicked = RoundedCornerButton( close_button_clicked = RoundedCornerButton(
@@ -300,17 +299,12 @@ int Render::TitleBar(bool main_window) {
} }
if (close_button_clicked) { if (close_button_clicked) {
#if _WIN32 const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
if (enable_minimize_to_tray_) { if (!minimized_to_tray) {
tray_->MinimizeToTray();
} else {
#endif
SDL_Event event; SDL_Event event;
event.type = SDL_EVENT_QUIT; event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event); SDL_PushEvent(&event);
#if _WIN32
} }
#endif
} }
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f, draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
@@ -329,4 +323,4 @@ int Render::TitleBar(bool main_window) {
ImGui::EndChild(); ImGui::EndChild();
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -5,14 +5,14 @@
#include <windows.h> #include <windows.h>
#endif #endif
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
bool Render::OpenUrl(const std::string& url) { bool GuiApplication::OpenUrl(const std::string &url) {
#if defined(_WIN32) #if defined(_WIN32)
int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0); int wide_len = MultiByteToWideChar(CP_UTF8, 0, url.c_str(), -1, nullptr, 0);
if (wide_len <= 0) { if (wide_len <= 0) {
@@ -46,8 +46,8 @@ bool Render::OpenUrl(const std::string& url) {
#endif #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) { const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255)); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::TextUnformatted(label.c_str()); ImGui::TextUnformatted(label.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -65,14 +65,14 @@ void Render::Hyperlink(const std::string& label, const std::string& url,
} }
} }
int Render::AboutWindow() { int GuiApplication::AboutWindow() {
if (show_about_window_) { if (show_about_window_) {
float about_window_width = title_bar_button_width_ * 7.5f; float about_window_width = title_bar_button_width_ * 7.5f;
float about_window_height = latest_version_.empty() float about_window_height = latest_version_.empty()
? title_bar_button_width_ * 4.0f ? title_bar_button_width_ * 4.0f
: title_bar_button_width_ * 4.9f; : title_bar_button_width_ * 4.9f;
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2( ImGui::SetNextWindowPos(ImVec2(
(viewport->WorkSize.x - viewport->WorkPos.x - about_window_width) / 2, (viewport->WorkSize.x - viewport->WorkPos.x - about_window_width) / 2,
(viewport->WorkSize.y - viewport->WorkPos.y - about_window_height) / (viewport->WorkSize.y - viewport->WorkPos.y - about_window_height) /
@@ -145,4 +145,4 @@ int Render::AboutWindow() {
} }
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,13 +1,13 @@
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
bool Render::ConnectionStatusWindow( bool GuiApplication::ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<RemoteSession> &props) {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
bool ret_flag = false; bool ret_flag = false;
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
@@ -31,8 +31,9 @@ bool Render::ConnectionStatusWindow(
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
std::string text; std::string text;
const ConnectionStatus status = props->connection_status_.load();
if (ConnectionStatus::Connecting == props->connection_status_) { if (ConnectionStatus::Connecting == status) {
text = localization::p2p_connecting[localization_language_index_]; text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -48,7 +49,23 @@ bool Render::ConnectionStatusWindow(
} }
ret_flag = true; ret_flag = true;
} }
} else if (ConnectionStatus::Connected == props->connection_status_) { } else if (ConnectionStatus::Gathering == status) {
text = localization::p2p_gathering[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == status) {
text = localization::p2p_connected[localization_language_index_]; text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -58,7 +75,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::Disconnected == props->connection_status_) { } else if (ConnectionStatus::Disconnected == status) {
text = localization::p2p_disconnected[localization_language_index_]; text = localization::p2p_disconnected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -68,7 +85,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::Failed == props->connection_status_) { } else if (ConnectionStatus::Failed == status) {
text = localization::p2p_failed[localization_language_index_]; text = localization::p2p_failed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -78,7 +95,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::Closed == props->connection_status_) { } else if (ConnectionStatus::Closed == status) {
text = localization::p2p_closed[localization_language_index_]; text = localization::p2p_closed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -88,7 +105,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::IncorrectPassword == props->connection_status_) { } else if (ConnectionStatus::IncorrectPassword == status) {
if (!password_validating_) { if (!password_validating_) {
if (password_validating_time_ == 1) { if (password_validating_time_ == 1) {
text = localization::input_password[localization_language_index_]; text = localization::input_password[localization_language_index_];
@@ -151,8 +168,7 @@ bool Render::ConnectionStatusWindow(
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
} }
} else if (ConnectionStatus::NoSuchTransmissionId == } else if (ConnectionStatus::NoSuchTransmissionId == status) {
props->connection_status_) {
text = localization::no_such_id[localization_language_index_]; text = localization::no_such_id[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -164,6 +180,19 @@ bool Render::ConnectionStatusWindow(
DestroyPeer(&props->peer_); DestroyPeer(&props->peer_);
ret_flag = true; 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; auto text_width = ImGui::CalcTextSize(text.c_str()).x;
@@ -177,4 +206,4 @@ bool Render::ConnectionStatusWindow(
return ret_flag; return ret_flag;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,9 +1,10 @@
#include "application/gui_application.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) { int GuiApplication::ControlWindow(
std::shared_ptr<RemoteSession> &props) {
double time_duration = double time_duration =
ImGui::GetTime() - props->control_bar_button_pressed_time_; ImGui::GetTime() - props->control_bar_button_pressed_time_;
if (props->control_window_width_is_changing_) { if (props->control_window_width_is_changing_) {
@@ -77,8 +78,10 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2 delta = ImGui::GetIO().MouseDelta; ImVec2 delta = ImGui::GetIO().MouseDelta;
pos_x = current_x_rel + delta.x; pos_x = current_x_rel + delta.x;
pos_y = current_y_rel + delta.y; pos_y = current_y_rel + delta.y;
if (pos_x < 0.0f) pos_x = 0.0f; if (pos_x < 0.0f)
if (pos_y < 0.0f) pos_y = 0.0f; pos_x = 0.0f;
if (pos_y < 0.0f)
pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w) if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_; pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h) 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; float current_y_rel = props->control_window_pos_.y - container_pos.y;
pos_x = current_x_rel; pos_x = current_x_rel;
pos_y = current_y_rel; pos_y = current_y_rel;
if (pos_x < 0.0f) pos_x = 0.0f; if (pos_x < 0.0f)
if (pos_y < 0.0f) pos_y = 0.0f; pos_x = 0.0f;
if (pos_y < 0.0f)
pos_y = 0.0f;
if (pos_x + props->control_window_width_ > container_w) if (pos_x + props->control_window_width_ > container_w)
pos_x = container_w - props->control_window_width_; pos_x = container_w - props->control_window_width_;
if (pos_y + props->control_window_height_ > container_h) if (pos_y + props->control_window_height_ > container_h)
@@ -256,4 +261,4 @@ int Render::ControlWindow(std::shared_ptr<SubStreamWindowProperties>& props) {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -2,16 +2,17 @@
#include <cmath> #include <cmath>
#include "IconsFontAwesome6.h" #include "IconsFontAwesome6.h"
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
namespace { namespace {
int CountDigits(int number) { int CountDigits(int number) {
if (number == 0) return 1; if (number == 0)
return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1; return (int)std::floor(std::log10(std::abs(number))) + 1;
} }
@@ -26,16 +27,16 @@ int BitrateDisplay(int bitrate) {
} }
return 0; return 0;
} }
} // namespace } // namespace
int Render::FileTransferWindow( int GuiApplication::FileTransferWindow(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<RemoteSession> &props) {
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_; FileTransferState *state = &transfers_.state_for(props);
state->file_transfer_window_hovered_ = false; state->file_transfer_window_hovered_ = false;
// Only show window if there are files in transfer list or currently // Only show window if there are files in transfer list or currently
// transferring // transferring
std::vector<SubStreamWindowProperties::FileTransferInfo> file_list; std::vector<RemoteSession::FileTransferInfo> file_list;
{ {
std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_); std::lock_guard<std::mutex> lock(state->file_transfer_list_mutex_);
file_list = state->file_transfer_list_; file_list = state->file_transfer_list_;
@@ -44,20 +45,20 @@ int Render::FileTransferWindow(
// Sort file list: Sending first, then Completed, then Queued, then Failed // Sort file list: Sending first, then Completed, then Queued, then Failed
std::sort( std::sort(
file_list.begin(), file_list.end(), file_list.begin(), file_list.end(),
[](const SubStreamWindowProperties::FileTransferInfo& a, [](const RemoteSession::FileTransferInfo &a,
const SubStreamWindowProperties::FileTransferInfo& b) { const RemoteSession::FileTransferInfo &b) {
// Priority: Sending > Completed > Queued > Failed // Priority: Sending > Completed > Queued > Failed
auto get_priority = auto get_priority =
[](SubStreamWindowProperties::FileTransferStatus status) { [](RemoteSession::FileTransferStatus status) {
switch (status) { switch (status) {
case SubStreamWindowProperties::FileTransferStatus::Sending: case RemoteSession::FileTransferStatus::Sending:
return 0; return 0;
case SubStreamWindowProperties::FileTransferStatus::Completed: case RemoteSession::FileTransferStatus::Completed:
return 1; return 1;
case SubStreamWindowProperties::FileTransferStatus::Queued: case RemoteSession::FileTransferStatus::Queued:
return 2; return 2;
case SubStreamWindowProperties::FileTransferStatus::Failed: case RemoteSession::FileTransferStatus::Failed:
return 3; return 3;
} }
return 3; return 3;
}; };
@@ -67,8 +68,8 @@ int Render::FileTransferWindow(
// Only show window if file_transfer_window_visible_ is true // Only show window if file_transfer_window_visible_ is true
// Window can be closed by user even during transfer // Window can be closed by user even during transfer
// It will be reopened automatically when: // It will be reopened automatically when:
// 1. A file transfer completes (in render_callback.cpp) // 1. A file transfer completes (via FileTransferManager)
// 2. A new file starts sending from queue (in render.cpp) // 2. FileTransferManager starts the next queued file.
if (!state->file_transfer_window_visible_) { if (!state->file_transfer_window_visible_) {
return 0; return 0;
} }
@@ -77,7 +78,7 @@ int Render::FileTransferWindow(
// Adjust window size based on number of files // Adjust window size based on number of files
float file_transfer_window_width = main_window_width_ * 0.6f; float file_transfer_window_width = main_window_width_ * 0.6f;
float file_transfer_window_height = float file_transfer_window_height =
main_window_height_ * 0.3f; // Dynamic height main_window_height_ * 0.3f; // Dynamic height
float pos_x = file_transfer_window_width * 0.05f; float pos_x = file_transfer_window_width * 0.05f;
float pos_y = stream_window_height_ - file_transfer_window_height - float pos_y = stream_window_height_ - file_transfer_window_height -
file_transfer_window_width * 0.05; file_transfer_window_width * 0.05;
@@ -147,40 +148,40 @@ int Render::FileTransferWindow(
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
for (size_t i = 0; i < file_list.size(); ++i) { for (size_t i = 0; i < file_list.size(); ++i) {
const auto& info = file_list[i]; const auto &info = file_list[i];
ImGui::PushID(static_cast<int>(i)); ImGui::PushID(static_cast<int>(i));
// Status icon and file name // Status icon and file name
const char* status_icon = ""; const char *status_icon = "";
ImVec4 status_color(0.5f, 0.5f, 0.5f, 1.0f); ImVec4 status_color(0.5f, 0.5f, 0.5f, 1.0f);
const char* status_text = ""; const char *status_text = "";
switch (info.status) { switch (info.status) {
case SubStreamWindowProperties::FileTransferStatus::Queued: case RemoteSession::FileTransferStatus::Queued:
status_icon = ICON_FA_CLOCK; status_icon = ICON_FA_CLOCK;
status_color = status_color =
ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued ImVec4(0.5f, 0.6f, 0.7f, 1.0f); // Common blue-gray for queued
status_text = status_text =
localization::queued[localization_language_index_].c_str(); localization::queued[localization_language_index_].c_str();
break; break;
case SubStreamWindowProperties::FileTransferStatus::Sending: case RemoteSession::FileTransferStatus::Sending:
status_icon = ICON_FA_ARROW_UP; status_icon = ICON_FA_ARROW_UP;
status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f); status_color = ImVec4(0.2f, 0.6f, 1.0f, 1.0f);
status_text = status_text =
localization::sending[localization_language_index_].c_str(); localization::sending[localization_language_index_].c_str();
break; break;
case SubStreamWindowProperties::FileTransferStatus::Completed: case RemoteSession::FileTransferStatus::Completed:
status_icon = ICON_FA_CHECK; status_icon = ICON_FA_CHECK;
status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f); status_color = ImVec4(0.0f, 0.8f, 0.0f, 1.0f);
status_text = status_text =
localization::completed[localization_language_index_].c_str(); localization::completed[localization_language_index_].c_str();
break; break;
case SubStreamWindowProperties::FileTransferStatus::Failed: case RemoteSession::FileTransferStatus::Failed:
status_icon = ICON_FA_XMARK; status_icon = ICON_FA_XMARK;
status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); status_color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f);
status_text = status_text =
localization::failed[localization_language_index_].c_str(); localization::failed[localization_language_index_].c_str();
break; break;
} }
ImGui::TextColored(status_color, "%s", status_icon); ImGui::TextColored(status_color, "%s", status_icon);
@@ -191,7 +192,7 @@ int Render::FileTransferWindow(
// Progress bar for sending files // Progress bar for sending files
if (info.status == if (info.status ==
SubStreamWindowProperties::FileTransferStatus::Sending && RemoteSession::FileTransferStatus::Sending &&
info.file_size > 0) { info.file_size > 0) {
float progress = static_cast<float>(info.sent_bytes) / float progress = static_cast<float>(info.sent_bytes) /
static_cast<float>(info.file_size); static_cast<float>(info.file_size);
@@ -210,7 +211,7 @@ int Render::FileTransferWindow(
ImGui::SetCursorPosX(speed_x_pos); ImGui::SetCursorPosX(speed_x_pos);
BitrateDisplay(static_cast<int>(info.rate_bps)); BitrateDisplay(static_cast<int>(info.rate_bps));
} else if (info.status == } else if (info.status ==
SubStreamWindowProperties::FileTransferStatus::Completed) { RemoteSession::FileTransferStatus::Completed) {
// Show completed size // Show completed size
char size_str[64]; char size_str[64];
if (info.file_size < 1024) { if (info.file_size < 1024) {
@@ -249,4 +250,4 @@ int Render::FileTransferWindow(
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,43 +1,56 @@
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
#include "tinyfiledialogs.h" #include "tinyfiledialogs.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include "service_host.h"
#endif
namespace crossdesk { namespace crossdesk {
int Render::SettingWindow() { int GuiApplication::SettingWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
float portable_y_padding = 0.0f;
#if _WIN32 && CROSSDESK_PORTABLE
portable_y_padding = 0.05f;
#endif
if (show_settings_window_) { if (show_settings_window_) {
if (settings_window_pos_reset_) { if (settings_window_pos_reset_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport *viewport = ImGui::GetMainViewport();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \ !defined(__arm__) && USE_CUDA) || \
defined(__APPLE__)) defined(__APPLE__))
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f)); ImVec2(io.DisplaySize.x * 0.315f,
io.DisplaySize.y * (0.9f + portable_y_padding)));
#else #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f)); ImVec2(io.DisplaySize.x * 0.315f,
io.DisplaySize.y * (0.85f + portable_y_padding)));
#endif #endif
} else { } else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \ !defined(__arm__) && USE_CUDA) || \
defined(__APPLE__)) defined(__APPLE__))
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f)); ImVec2(io.DisplaySize.x * 0.42f,
io.DisplaySize.y * (0.9f + portable_y_padding)));
#else #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f)); ImVec2(io.DisplaySize.x * 0.42f,
io.DisplaySize.y * (0.85f + portable_y_padding)));
#endif #endif
} }
@@ -60,7 +73,7 @@ int Render::SettingWindow() {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
{ {
const auto& supported_languages = localization::GetSupportedLanguages(); const auto &supported_languages = localization::GetSupportedLanguages();
language_button_value_ = language_button_value_ =
localization::detail::ClampLanguageIndex(language_button_value_); localization::detail::ClampLanguageIndex(language_button_value_);
@@ -73,23 +86,21 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo( if (ImGui::BeginCombo("##language",
"##language", localization::GetSupportedLanguages()
localization::GetSupportedLanguages() [localization::detail::ClampLanguageIndex(
[localization::detail::ClampLanguageIndex( language_button_value_)]
language_button_value_)] .display_name.c_str())) {
.display_name
.c_str())) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < static_cast<int>(supported_languages.size()); for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) { ++i) {
bool selected = (i == language_button_value_); bool selected = (i == language_button_value_);
if (ImGui::Selectable( if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
supported_languages[i].display_name.c_str(), selected)) selected))
language_button_value_ = i; language_button_value_ = i;
if (selected) { if (selected) {
ImGui::SetItemDefaultFocus(); ImGui::SetItemDefaultFocus();
@@ -107,7 +118,7 @@ int Render::SettingWindow() {
} }
{ {
const char* video_quality_items[] = { const char *video_quality_items[] = {
localization::video_quality_low[localization_language_index_] localization::video_quality_low[localization_language_index_]
.c_str(), .c_str(),
localization::video_quality_medium[localization_language_index_] localization::video_quality_medium[localization_language_index_]
@@ -125,7 +136,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -146,7 +157,7 @@ int Render::SettingWindow() {
ImGui::Separator(); ImGui::Separator();
{ {
const char* video_frame_rate_items[] = {"30 fps", "60 fps"}; const char *video_frame_rate_items[] = {"30 fps", "60 fps"};
settings_items_offset += settings_items_padding; settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
@@ -158,7 +169,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -179,7 +190,7 @@ int Render::SettingWindow() {
ImGui::Separator(); ImGui::Separator();
{ {
const char* video_encode_format_items[] = { const char *video_encode_format_items[] = {
localization::h264[localization_language_index_].c_str(), localization::h264[localization_language_index_].c_str(),
localization::av1[localization_language_index_].c_str()}; localization::av1[localization_language_index_].c_str()};
@@ -194,7 +205,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
} }
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -212,8 +223,8 @@ int Render::SettingWindow() {
} }
} }
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \ !defined(__arm__) && USE_CUDA) || \
defined(__APPLE__)) defined(__APPLE__))
ImGui::Separator(); ImGui::Separator();
@@ -228,7 +239,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_hardware_video_codec", ImGui::Checkbox("##enable_hardware_video_codec",
@@ -249,7 +260,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_turn", &enable_turn_); ImGui::Checkbox("##enable_turn", &enable_turn_);
@@ -268,7 +279,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_srtp", &enable_srtp_); ImGui::Checkbox("##enable_srtp", &enable_srtp_);
@@ -289,7 +300,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_); ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
@@ -308,7 +319,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_autostart_", &enable_autostart_); ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
@@ -327,7 +338,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_daemon_", &enable_daemon_); ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
@@ -345,10 +356,6 @@ int Render::SettingWindow() {
ImGui::Separator(); ImGui::Separator();
{ {
#ifndef _WIN32
ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif
settings_items_offset += settings_items_padding; settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
@@ -359,15 +366,11 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_minimize_to_tray_", ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_); &enable_minimize_to_tray_);
#ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
#endif
} }
ImGui::Separator(); ImGui::Separator();
@@ -384,7 +387,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f); ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
} }
std::string display_path = std::string display_path =
@@ -399,7 +402,7 @@ int Render::SettingWindow() {
ImGui::PushFont(main_windows_system_chinese_font_); ImGui::PushFont(main_windows_system_chinese_font_);
if (ImGui::Button(display_path.c_str(), if (ImGui::Button(display_path.c_str(),
ImVec2(title_bar_button_width_ * 2.0f, 0))) { ImVec2(title_bar_button_width_ * 2.0f, 0))) {
const char* folder = const char *folder =
tinyfd_selectFolderDialog(localization::file_transfer_save_path tinyfd_selectFolderDialog(localization::file_transfer_save_path
[localization_language_index_] [localization_language_index_]
.c_str(), .c_str(),
@@ -429,6 +432,80 @@ int Render::SettingWindow() {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
#if _WIN32 && CROSSDESK_PORTABLE
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding();
ImGui::Text("%s", localization::windows_service_settings_label
[localization_language_index_]
.c_str());
ImGui::SameLine();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f);
} else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
}
const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire);
const bool service_installed =
IsCrossDeskServiceInstalled() ||
state == PortableServiceInstallState::succeeded;
if (service_installed) {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f);
} else if (ConfigCenter::LANGUAGE::ENGLISH ==
localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f);
} else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
}
ImGui::Text("%s", localization::windows_service_installed
[localization_language_index_]
.c_str());
} else {
if (state == PortableServiceInstallState::installing) {
ImGui::BeginDisabled();
}
if (ImGui::Button(localization::install_windows_service
[localization_language_index_]
.c_str())) {
StartPortableWindowsServiceInstall();
}
if (state == PortableServiceInstallState::installing) {
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::Text("%s", localization::installing_windows_service
[localization_language_index_]
.c_str());
} else if (state == PortableServiceInstallState::failed) {
ImGui::SameLine();
ImGui::Text(
"%s",
localization::failed[localization_language_index_].c_str());
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f);
ImGui::TextWrapped("%s",
localization::windows_service_install_failed
[localization_language_index_]
.c_str());
ImGui::PopTextWrapPos();
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
}
}
#endif
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f); ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
} else { } else {
@@ -436,7 +513,7 @@ int Render::SettingWindow() {
} }
settings_items_offset += settings_items_offset +=
settings_items_padding + title_bar_button_width_ * 0.3f; settings_items_padding + title_bar_button_width_ * 0.15f;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -463,9 +540,8 @@ int Render::SettingWindow() {
LOG_INFO("Set localization language: {}", LOG_INFO("Set localization language: {}",
localization::GetSupportedLanguages() localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex( [localization::detail::ClampLanguageIndex(
localization_language_index_)] localization_language_index_)]
.code .code.c_str());
.c_str());
// Video quality // Video quality
if (video_quality_button_value_ == 0) { if (video_quality_button_value_ == 0) {
@@ -542,14 +618,12 @@ int Render::SettingWindow() {
} }
enable_daemon_last_ = enable_daemon_; enable_daemon_last_ = enable_daemon_;
#if _WIN32
if (enable_minimize_to_tray_) { if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true); config_center_->SetMinimizeToTray(true);
} else { } else {
config_center_->SetMinimizeToTray(false); config_center_->SetMinimizeToTray(false);
} }
enable_minimize_to_tray_last_ = enable_minimize_to_tray_; enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif
// File transfer save path // File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_); config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
@@ -558,12 +632,12 @@ int Render::SettingWindow() {
settings_window_pos_reset_ = true; settings_window_pos_reset_ = true;
// Recreate peer instance // Recreate peer instance
LoadSettingsFromCacheFile(); settings_.Load();
// Recreate peer instance // Recreate peer instance
if (!stream_window_inited_) { if (!stream_window_inited_) {
LOG_INFO("Recreate peer instance"); LOG_INFO("Recreate peer instance");
CleanupPeers(); CloseAllRemoteSessions();
CreateConnectionPeer(); CreateConnectionPeer();
} }
} }
@@ -620,4 +694,4 @@ int Render::SettingWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,12 +1,12 @@
#include "application/gui_application.h"
#include "layout_relative.h" #include "layout_relative.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
int Render::MainWindow() { int GuiApplication::MainWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
float local_remote_window_width = io.DisplaySize.x; float local_remote_window_width = io.DisplaySize.x;
float local_remote_window_height = float local_remote_window_height =
io.DisplaySize.y * (0.56f - TITLE_BAR_HEIGHT); io.DisplaySize.y * (0.56f - TITLE_BAR_HEIGHT);
@@ -27,7 +27,7 @@ int Render::MainWindow() {
LocalWindow(); LocalWindow();
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList *draw_list = ImGui::GetWindowDrawList();
draw_list->AddLine(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.1f), draw_list->AddLine(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.1f),
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.53f), ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.53f),
IM_COL32(0, 0, 0, 122), 1.0f); IM_COL32(0, 0, 0, 122), 1.0f);
@@ -39,13 +39,13 @@ int Render::MainWindow() {
StatusBar(); StatusBar();
if (show_connection_status_window_) { if (show_connection_status_window_) {
// std::unique_lock lock(client_properties_mutex_); // std::unique_lock lock(remote_sessions_mutex_);
for (auto it = client_properties_.begin(); for (auto it = remote_sessions_.begin();
it != client_properties_.end();) { it != remote_sessions_.end();) {
auto& props = it->second; auto &props = it->second;
if (focused_remote_id_ == props->remote_id_) { if (focused_remote_id_ == props->remote_id_) {
if (ConnectionStatusWindow(props)) { if (ConnectionStatusWindow(props)) {
it = client_properties_.erase(it); it = remote_sessions_.erase(it);
} else { } else {
++it; ++it;
} }
@@ -57,4 +57,4 @@ int Render::MainWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,9 +1,10 @@
#include "render.h" #include "application/gui_application.h"
#if _WIN32 && CROSSDESK_PORTABLE #if _WIN32 && CROSSDESK_PORTABLE
#include <shellapi.h> #include <shellapi.h>
#include <algorithm>
#include <vector> #include <vector>
#include "localization.h" #include "localization.h"
@@ -16,9 +17,8 @@ namespace {
std::filesystem::path GetCurrentExecutablePath() { std::filesystem::path GetCurrentExecutablePath() {
std::vector<wchar_t> buffer(MAX_PATH); std::vector<wchar_t> buffer(MAX_PATH);
while (true) { while (true) {
DWORD length = DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
static_cast<DWORD>(buffer.size()));
if (length == 0) { if (length == 0) {
return {}; return {};
} }
@@ -45,10 +45,10 @@ bool InstallServiceWithElevation() {
executable_path.parent_path() / L"crossdesk_session_helper.exe"; executable_path.parent_path() / L"crossdesk_session_helper.exe";
if (!std::filesystem::exists(service_path) || if (!std::filesystem::exists(service_path) ||
!std::filesystem::exists(helper_path)) { !std::filesystem::exists(helper_path)) {
LOG_ERROR( LOG_ERROR("Portable service install failed: service binaries missing, "
"Portable service install failed: service binaries missing, service={}, " "service={}, "
"helper={}", "helper={}",
service_path.string(), helper_path.string()); service_path.string(), helper_path.string());
return false; return false;
} }
@@ -94,9 +94,9 @@ bool InstallServiceWithElevation() {
return IsCrossDeskServiceInstalled() && started; return IsCrossDeskServiceInstalled() && started;
} }
} // namespace } // namespace
void Render::CheckPortableWindowsService() { void GuiApplication::CheckPortableWindowsService() {
if (portable_service_prompt_checked_) { if (portable_service_prompt_checked_) {
return; return;
} }
@@ -106,12 +106,17 @@ void Render::CheckPortableWindowsService() {
return; return;
} }
if (portable_service_prompt_suppressed_) {
return;
}
portable_service_install_state_.store(PortableServiceInstallState::idle, portable_service_install_state_.store(PortableServiceInstallState::idle,
std::memory_order_relaxed); std::memory_order_relaxed);
show_portable_service_install_window_ = true; show_portable_service_install_window_ = true;
} }
void Render::StartPortableWindowsServiceInstall() { void GuiApplication::StartPortableWindowsServiceInstall() {
portable_service_do_not_remind_ = false;
PortableServiceInstallState expected = PortableServiceInstallState::idle; PortableServiceInstallState expected = PortableServiceInstallState::idle;
if (!portable_service_install_state_.compare_exchange_strong( if (!portable_service_install_state_.compare_exchange_strong(
expected, PortableServiceInstallState::installing, expected, PortableServiceInstallState::installing,
@@ -133,25 +138,89 @@ void Render::StartPortableWindowsServiceInstall() {
}); });
} }
void Render::JoinPortableWindowsServiceInstallThread() { void GuiApplication::JoinPortableWindowsServiceInstallThread() {
if (portable_service_install_thread_.joinable()) { if (portable_service_install_thread_.joinable()) {
portable_service_install_thread_.join(); 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;
}
const ImGuiViewport *viewport = ImGui::GetMainViewport();
const float window_width =
(std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f);
const float window_height =
(std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f);
if (show_portable_service_prompt_suppressed_window_) {
const float notice_width = window_width;
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,
viewport->WorkPos.y +
(viewport->WorkSize.y - notice_height) / 2.0f),
ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(notice_width, notice_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::Begin(
localization::notification[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoTitleBar);
ImGui::Spacing();
ImGui::SetWindowFontScale(0.55f);
ImGui::SetCursorPosX(notice_width * 0.08f);
ImGui::Text(
"%s", localization::notification[localization_language_index_].c_str());
ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(notice_width * 0.06f);
ImGui::SetCursorPosY(notice_height * 0.28f);
ImGui::PushTextWrapPos(notice_width * 0.88f);
ImGui::TextWrapped("%s",
localization::windows_service_prompt_suppressed_message
[localization_language_index_]
.c_str());
ImGui::PopTextWrapPos();
const std::string ok_label = localization::ok[localization_language_index_];
const ImGuiStyle &style = ImGui::GetStyle();
const float ok_width =
ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f;
ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f);
ImGui::SetCursorPosY(notice_height * 0.75f);
if (ImGui::Button(ok_label.c_str())) {
show_portable_service_prompt_suppressed_window_ = false;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
if (!show_portable_service_install_window_) { if (!show_portable_service_install_window_) {
return 0; return 0;
} }
const ImGuiViewport* viewport = ImGui::GetMainViewport();
const float window_width = title_bar_button_width_ * 12.0f;
const float window_height = title_bar_button_width_ * 4.0f;
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) / ImVec2(viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
2.0f, viewport->WorkPos.y +
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) / (viewport->WorkSize.y - window_height) / 2.0f),
2.0f),
ImGuiCond_Appearing); ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
ImGuiCond_Always); ImGuiCond_Always);
@@ -179,7 +248,7 @@ int Render::PortableServiceInstallWindow() {
const PortableServiceInstallState state = const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire); portable_service_install_state_.load(std::memory_order_acquire);
const char* status_text = nullptr; const char *status_text = nullptr;
if (state == PortableServiceInstallState::installing || if (state == PortableServiceInstallState::installing ||
state == PortableServiceInstallState::succeeded || state == PortableServiceInstallState::succeeded ||
state == PortableServiceInstallState::failed) { state == PortableServiceInstallState::failed) {
@@ -187,15 +256,13 @@ int Render::PortableServiceInstallWindow() {
localization::installing_windows_service[localization_language_index_] localization::installing_windows_service[localization_language_index_]
.c_str(); .c_str();
if (state == PortableServiceInstallState::succeeded) { if (state == PortableServiceInstallState::succeeded) {
status_text = status_text = localization::windows_service_install_success
localization::windows_service_install_success [localization_language_index_]
[localization_language_index_] .c_str();
.c_str();
} else if (state == PortableServiceInstallState::failed) { } else if (state == PortableServiceInstallState::failed) {
status_text = status_text = localization::windows_service_install_failed
localization::windows_service_install_failed [localization_language_index_]
[localization_language_index_] .c_str();
.c_str();
} }
} }
@@ -203,7 +270,7 @@ int Render::PortableServiceInstallWindow() {
ImGui::SetCursorPosX(window_width * 0.04f); ImGui::SetCursorPosX(window_width * 0.04f);
ImGui::SetCursorPosY(window_height * 0.22f); ImGui::SetCursorPosY(window_height * 0.22f);
ImGui::BeginChild("PortableServiceInstallContent", ImGui::BeginChild("PortableServiceInstallContent",
ImVec2(window_width * 0.92f, window_height * 0.5f), ImVec2(window_width * 0.92f, window_height * 0.45f),
ImGuiChildFlags_Borders, ImGuiWindowFlags_None); ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
const float wrap_pos = ImGui::GetContentRegionAvail().x; const float wrap_pos = ImGui::GetContentRegionAvail().x;
@@ -220,9 +287,17 @@ int Render::PortableServiceInstallWindow() {
ImGui::EndChild(); ImGui::EndChild();
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
const float button_y = window_height * 0.76f; ImGui::SetCursorPosX(window_width * 0.08f);
const ImGuiStyle& style = ImGui::GetStyle(); ImGui::SetCursorPosY(window_height * 0.71f);
const auto default_button_width = [&style](const std::string& label) { ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
ImGui::Checkbox(
localization::do_not_remind_again[localization_language_index_].c_str(),
&portable_service_do_not_remind_);
ImGui::PopStyleVar();
const float button_y = window_height * 0.84f;
const ImGuiStyle &style = ImGui::GetStyle();
const auto default_button_width = [&style](const std::string &label) {
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f; return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
}; };
const std::string install_label = const std::string install_label =
@@ -259,6 +334,11 @@ int Render::PortableServiceInstallWindow() {
ImGui::BeginDisabled(); ImGui::BeginDisabled();
} }
if (ImGui::Button(cancel_label.c_str())) { if (ImGui::Button(cancel_label.c_str())) {
if (portable_service_do_not_remind_) {
portable_service_prompt_suppressed_ = true;
config_center_->SetPortableServicePromptSuppressed(true);
show_portable_service_prompt_suppressed_window_ = true;
}
show_portable_service_install_window_ = false; show_portable_service_install_window_ = false;
} }
if (state == PortableServiceInstallState::installing) { if (state == PortableServiceInstallState::installing) {
@@ -274,6 +354,6 @@ int Render::PortableServiceInstallWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
#endif #endif
@@ -1,17 +1,11 @@
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h"
#include "render.h"
#include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h>
#include <unistd.h>
#include <cstdlib>
namespace crossdesk { namespace crossdesk {
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) { bool GuiApplication::DrawToggleSwitch(const char *id, bool active,
bool enabled) {
const float TRACK_HEIGHT = ImGui::GetFrameHeight(); const float TRACK_HEIGHT = ImGui::GetFrameHeight();
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f; const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f; const float TRACK_RADIUS = TRACK_HEIGHT * 0.5f;
@@ -28,23 +22,26 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
const ImVec4 COLOR_INACTIVE_HOVER = ImVec4(0.70f, 0.70f, 0.70f, 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); const ImVec4 COLOR_KNOB = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList *draw_list = ImGui::GetWindowDrawList();
ImVec2 track_pos = ImGui::GetCursorScreenPos(); ImVec2 track_pos = ImGui::GetCursorScreenPos();
ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT)); ImGui::InvisibleButton(id, ImVec2(TRACK_WIDTH, TRACK_HEIGHT));
bool hovered = ImGui::IsItemHovered(); bool hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked() && enabled; bool clicked = ImGui::IsItemClicked() && enabled;
ImVec4 track_color = active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE) ImVec4 track_color =
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE); active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
if (!enabled) { if (!enabled) {
track_color.w *= DISABLED_ALPHA; track_color.w *= DISABLED_ALPHA;
} }
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f); 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); ImVec2 track_max =
draw_list->AddRectFilled(track_min, track_max, ImGui::GetColorU32(track_color), TRACK_RADIUS); 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_position = active ? 1.0f : 0.0f;
float knob_min_x = track_pos.x + KNOB_PADDING; float knob_min_x = track_pos.x + KNOB_PADDING;
@@ -59,71 +56,45 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
ImVec2 knob_min = ImVec2(knob_x, knob_y); ImVec2 knob_min = ImVec2(knob_x, knob_y);
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT); 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); draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color),
KNOB_RADIUS);
return clicked; return clicked;
} }
bool Render::CheckScreenRecordingPermission() { int GuiApplication::RequestPermissionWindow() {
// CGPreflightScreenCaptureAccess is available on macOS 10.15+ RefreshMacPermissionStatus(false);
if (@available(macOS 10.15, *)) {
bool granted = CGPreflightScreenCaptureAccess();
return granted;
}
// for older macOS versions, assume permission is granted
return true;
}
bool Render::CheckAccessibilityPermission() { const bool screen_recording_granted =
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @NO}; mac_screen_recording_permission_granted_;
bool trusted = AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); const bool accessibility_granted = mac_accessibility_permission_granted_;
return trusted;
}
void Render::OpenAccessibilityPreferences() { show_request_permission_window_ =
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES}; !screen_recording_granted || !accessibility_granted;
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
system("open "
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
"Accessibility\"");
}
void Render::OpenScreenRecordingPreferences() {
if (@available(macOS 10.15, *)) {
CGRequestScreenCaptureAccess();
}
system("open "
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
"ScreenCapture\"");
}
int Render::RequestPermissionWindow() {
bool screen_recording_granted = CheckScreenRecordingPermission();
bool accessibility_granted = CheckAccessibilityPermission();
show_request_permission_window_ = !screen_recording_granted || !accessibility_granted;
if (!show_request_permission_window_) { if (!show_request_permission_window_) {
return 0; return 0;
} }
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport *viewport = ImGui::GetMainViewport();
float window_width = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_WIDTH_CN float window_width = localization_language_index_ == 0
: REQUEST_PERMISSION_WINDOW_WIDTH_EN; ? REQUEST_PERMISSION_WINDOW_WIDTH_CN
float window_height = localization_language_index_ == 0 ? REQUEST_PERMISSION_WINDOW_HEIGHT_CN : REQUEST_PERMISSION_WINDOW_WIDTH_EN;
: REQUEST_PERMISSION_WINDOW_HEIGHT_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 float checkbox_padding = localization_language_index_ == 0
? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN ? REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN
: REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN; : REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN;
ImVec2 center_pos = ImVec2((viewport->WorkSize.x - window_width) * 0.5f + viewport->WorkPos.x, ImVec2 center_pos = ImVec2(
(viewport->WorkSize.y - window_height) * 0.5f + viewport->WorkPos.y); (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::SetNextWindowPos(center_pos, ImGuiCond_Once);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
ImGuiCond_Always);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
@@ -132,8 +103,10 @@ int Render::RequestPermissionWindow() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin( ImGui::Begin(
localization::request_permissions[localization_language_index_].c_str(), nullptr, localization::request_permissions[localization_language_index_].c_str(),
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings); nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(0.3f); ImGui::SetWindowFontScale(0.3f);
@@ -142,10 +115,13 @@ int Render::RequestPermissionWindow() {
ImGui::PushFont(main_windows_system_chinese_font_); ImGui::PushFont(main_windows_system_chinese_font_);
} }
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() + 5.0f); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeight() +
5.0f);
ImGui::SetCursorPosX(10.0f); ImGui::SetCursorPosX(10.0f);
ImGui::TextWrapped( ImGui::TextWrapped(
"%s", localization::permission_required_message[localization_language_index_].c_str()); "%s",
localization::permission_required_message[localization_language_index_]
.c_str());
ImGui::Spacing(); ImGui::Spacing();
ImGui::Spacing(); ImGui::Spacing();
@@ -154,16 +130,20 @@ int Render::RequestPermissionWindow() {
// accessibility permission // accessibility permission
ImGui::SetCursorPosX(10.0f); ImGui::SetCursorPosX(10.0f);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::Text("1. %s:", ImGui::Text(
localization::accessibility_permission[localization_language_index_].c_str()); "1. %s:",
localization::accessibility_permission[localization_language_index_]
.c_str());
ImGui::SameLine(); ImGui::SameLine();
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::SetCursorPosX(checkbox_padding); ImGui::SetCursorPosX(checkbox_padding);
if (accessibility_granted) { if (accessibility_granted) {
DrawToggleSwitch("accessibility_toggle_on", true, false); DrawToggleSwitch("accessibility_toggle_on", true, false);
} else { } else {
if (DrawToggleSwitch("accessibility_toggle", accessibility_granted, !accessibility_granted)) { if (DrawToggleSwitch("accessibility_toggle", false, true)) {
OpenAccessibilityPreferences(); OpenAccessibilityPreferences();
mac_accessibility_permission_requested_ = true;
RefreshMacPermissionStatus(true);
} }
} }
@@ -172,18 +152,20 @@ int Render::RequestPermissionWindow() {
// screen recording permission // screen recording permission
ImGui::SetCursorPosX(10.0f); ImGui::SetCursorPosX(10.0f);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::Text("2. %s:", ImGui::Text(
localization::screen_recording_permission[localization_language_index_].c_str()); "2. %s:",
localization::screen_recording_permission[localization_language_index_]
.c_str());
ImGui::SameLine(); ImGui::SameLine();
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::SetCursorPosX(checkbox_padding); ImGui::SetCursorPosX(checkbox_padding);
if (screen_recording_granted) { if (screen_recording_granted) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
DrawToggleSwitch("screen_recording_toggle_on", true, false); DrawToggleSwitch("screen_recording_toggle_on", true, false);
} else { } else {
if (DrawToggleSwitch("screen_recording_toggle", screen_recording_granted, if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
!screen_recording_granted)) {
OpenScreenRecordingPreferences(); OpenScreenRecordingPreferences();
mac_screen_recording_permission_requested_ = true;
RefreshMacPermissionStatus(true);
} }
} }
@@ -202,4 +184,4 @@ int Render::RequestPermissionWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -5,10 +5,10 @@
#include <windows.h> #include <windows.h>
#endif #endif
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
@@ -28,8 +28,8 @@ std::vector<std::string> GetRootEntries() {
return roots; return roots;
} }
int Render::SelfHostedServerWindow() { int GuiApplication::SelfHostedServerWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
if (show_self_hosted_server_config_window_) { if (show_self_hosted_server_config_window_) {
if (self_hosted_server_config_window_pos_reset_) { if (self_hosted_server_config_window_pos_reset_) {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
@@ -194,4 +194,4 @@ int Render::SelfHostedServerWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,20 +1,23 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <memory>
#include <shared_mutex>
#include <string> #include <string>
#include <vector> #include <vector>
#include "application/gui_application.h"
#include "layout_relative.h" #include "layout_relative.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
#include "rounded_corner_button.h" #include "rounded_corner_button.h"
namespace crossdesk { namespace crossdesk {
namespace { namespace {
int CountDigits(int number) { int CountDigits(int number) {
if (number == 0) return 1; if (number == 0)
return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1; return (int)std::floor(std::log10(std::abs(number))) + 1;
} }
@@ -43,9 +46,9 @@ std::string FormatBytes(uint64_t bytes) {
} }
return std::string(buf); return std::string(buf);
} }
} // namespace } // namespace
int Render::ServerWindow() { int GuiApplication::ServerWindow() {
ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_), ImGui::SetNextWindowSize(ImVec2(server_window_width_, server_window_height_),
ImGuiCond_Always); ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
@@ -83,7 +86,7 @@ int Render::ServerWindow() {
ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
const char* icon = const char *icon =
server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP; server_window_collapsed_ ? ICON_FA_ANGLE_DOWN : ICON_FA_ANGLE_UP;
std::string toggle_label = std::string(icon) + "##server_toggle"; std::string toggle_label = std::string(icon) + "##server_toggle";
@@ -130,7 +133,7 @@ int Render::ServerWindow() {
return 0; return 0;
} }
int Render::RemoteClientInfoWindow() { int GuiApplication::RemoteClientInfoWindow() {
float remote_client_info_window_width = server_window_width_ * 0.8f; float remote_client_info_window_width = server_window_width_ * 0.8f;
float remote_client_info_window_height = float remote_client_info_window_height =
(server_window_height_ - server_window_title_bar_height_) * 0.9f; (server_window_height_ - server_window_title_bar_height_) * 0.9f;
@@ -149,19 +152,22 @@ int Render::RemoteClientInfoWindow() {
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f; float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::pair<std::string, std::string>> remote_entries; std::vector<std::pair<std::string, std::string>> remote_entries;
remote_entries.reserve(connection_status_.size()); {
for (const auto& kv : connection_status_) { std::shared_lock lock(connection_status_mutex_);
const auto host_it = connection_host_names_.find(kv.first); remote_entries.reserve(connection_status_.size());
const std::string display_name = for (const auto &kv : connection_status_) {
(host_it != connection_host_names_.end() && !host_it->second.empty()) const auto host_it = connection_host_names_.find(kv.first);
? host_it->second const std::string display_name =
: kv.first; (host_it != connection_host_names_.end() && !host_it->second.empty())
remote_entries.emplace_back(kv.first, display_name); ? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
}
} }
auto find_display_name_by_remote_id = auto find_display_name_by_remote_id =
[&remote_entries](const std::string& remote_id) -> std::string { [&remote_entries](const std::string &remote_id) -> std::string {
for (const auto& entry : remote_entries) { for (const auto &entry : remote_entries) {
if (entry.first == remote_id) { if (entry.first == remote_id) {
return entry.second; return entry.second;
} }
@@ -188,7 +194,7 @@ int Render::RemoteClientInfoWindow() {
localization::controller[localization_language_index_].c_str()); localization::controller[localization_language_index_].c_str());
ImGui::SameLine(); ImGui::SameLine();
const char* selected_preview = "-"; const char *selected_preview = "-";
if (!selected_server_remote_hostname_.empty()) { if (!selected_server_remote_hostname_.empty()) {
selected_preview = selected_server_remote_hostname_.c_str(); selected_preview = selected_server_remote_hostname_.c_str();
} else if (!remote_client_id_.empty()) { } else if (!remote_client_id_.empty()) {
@@ -220,10 +226,14 @@ int Render::RemoteClientInfoWindow() {
ImGui::SetWindowFontScale(font_scale); ImGui::SetWindowFontScale(font_scale);
if (!selected_server_remote_id_.empty()) { if (!selected_server_remote_id_.empty()) {
auto it = connection_status_.find(selected_server_remote_id_); ConnectionStatus status = ConnectionStatus::Closed;
const ConnectionStatus status = (it == connection_status_.end()) {
? ConnectionStatus::Closed std::shared_lock lock(connection_status_mutex_);
: it->second; auto it = connection_status_.find(selected_server_remote_id_);
if (it != connection_status_.end()) {
status = it->second;
}
}
ImGui::Text( ImGui::Text(
"%s", "%s",
@@ -231,36 +241,33 @@ int Render::RemoteClientInfoWindow() {
ImGui::SameLine(); ImGui::SameLine();
switch (status) { switch (status) {
case ConnectionStatus::Connected: case ConnectionStatus::Connected:
ImGui::Text( ImGui::Text(
"%s", "%s",
localization::p2p_connected[localization_language_index_].c_str()); localization::p2p_connected[localization_language_index_].c_str());
break; break;
case ConnectionStatus::Connecting: case ConnectionStatus::Connecting:
ImGui::Text( ImGui::Text(
"%s", "%s",
localization::p2p_connecting[localization_language_index_].c_str()); localization::p2p_connecting[localization_language_index_].c_str());
break; break;
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
ImGui::Text("%s", ImGui::Text(
localization::p2p_disconnected[localization_language_index_] "%s",
.c_str()); localization::p2p_disconnected[localization_language_index_].c_str());
break; break;
case ConnectionStatus::Failed: case ConnectionStatus::Failed:
ImGui::Text( ImGui::Text(
"%s", "%s", localization::p2p_failed[localization_language_index_].c_str());
localization::p2p_failed[localization_language_index_].c_str()); break;
break; case ConnectionStatus::Closed:
case ConnectionStatus::Closed: ImGui::Text(
ImGui::Text( "%s", localization::p2p_closed[localization_language_index_].c_str());
"%s", break;
localization::p2p_closed[localization_language_index_].c_str()); default:
break; ImGui::Text(
default: "%s", localization::p2p_failed[localization_language_index_].c_str());
ImGui::Text( break;
"%s",
localization::p2p_failed[localization_language_index_].c_str());
break;
} }
} }
@@ -278,16 +285,18 @@ int Render::RemoteClientInfoWindow() {
std::string path = OpenFileDialog(title); std::string path = OpenFileDialog(title);
LOG_INFO("Selected file path: {}", path.c_str()); 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(); ImGui::SameLine();
const bool is_sending = file_transfer_.file_sending_.load(); const bool is_sending = file_transfer.file_sending_.load();
if (is_sending) { if (is_sending) {
// Simple animation: cycle icon every 0.5s while sending. // Simple animation: cycle icon every 0.5s while sending.
static const char* kFileTransferIcons[] = {ICON_FA_ANGLE_UP, static const char *kFileTransferIcons[] = {ICON_FA_ANGLE_UP,
ICON_FA_ANGLES_UP}; ICON_FA_ANGLES_UP};
const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) % const int icon_index = static_cast<int>(ImGui::GetTime() / 0.5) %
(static_cast<int>(sizeof(kFileTransferIcons) / (static_cast<int>(sizeof(kFileTransferIcons) /
@@ -299,9 +308,9 @@ int Render::RemoteClientInfoWindow() {
} }
if (ImGui::IsItemHovered()) { if (ImGui::IsItemHovered()) {
const uint64_t sent_bytes = file_transfer_.file_sent_bytes_.load(); const uint64_t sent_bytes = file_transfer.file_sent_bytes_.load();
const uint64_t total_bytes = file_transfer_.file_total_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 uint32_t rate_bps = file_transfer.file_send_rate_bps_.load();
float progress = 0.0f; float progress = 0.0f;
if (total_bytes > 0) { if (total_bytes > 0) {
@@ -311,11 +320,11 @@ int Render::RemoteClientInfoWindow() {
} }
std::string current_file_name; 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) { if (current_file_id != 0) {
std::lock_guard<std::mutex> lock( std::lock_guard<std::mutex> lock(
file_transfer_.file_transfer_list_mutex_); file_transfer.file_transfer_list_mutex_);
for (const auto& info : file_transfer_.file_transfer_list_) { for (const auto &info : file_transfer.file_transfer_list_) {
if (info.file_id == current_file_id) { if (info.file_id == current_file_id) {
current_file_name = info.file_name; current_file_name = info.file_name;
break; break;
@@ -376,4 +385,4 @@ int Render::RemoteClientInfoWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,24 +1,24 @@
#include "application/gui_application.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
void Render::DrawConnectionStatusText( void GuiApplication::DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<RemoteSession> &props) {
std::string text; std::string text;
switch (props->connection_status_) { switch (props->connection_status_.load()) {
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
text = localization::p2p_disconnected[localization_language_index_]; text = localization::p2p_disconnected[localization_language_index_];
break; break;
case ConnectionStatus::Failed: case ConnectionStatus::Failed:
text = localization::p2p_failed[localization_language_index_]; text = localization::p2p_failed[localization_language_index_];
break; break;
case ConnectionStatus::Closed: case ConnectionStatus::Closed:
text = localization::p2p_closed[localization_language_index_]; text = localization::p2p_closed[localization_language_index_];
break; break;
default: default:
break; break;
} }
if (!text.empty()) { if (!text.empty()) {
@@ -31,10 +31,10 @@ void Render::DrawConnectionStatusText(
} }
} }
void Render::DrawReceivingScreenText( void GuiApplication::DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<RemoteSession> &props) {
if (!props->connection_established_ || if (!props->connection_established_ ||
props->connection_status_ != ConnectionStatus::Connected) { props->connection_status_.load() != ConnectionStatus::Connected) {
return; return;
} }
@@ -50,7 +50,7 @@ void Render::DrawReceivingScreenText(
return; return;
} }
const std::string& text = const std::string &text =
localization::receiving_screen[localization_language_index_]; localization::receiving_screen[localization_language_index_];
ImVec2 size = ImGui::GetWindowSize(); ImVec2 size = ImGui::GetWindowSize();
ImVec2 text_size = ImGui::CalcTextSize(text.c_str()); ImVec2 text_size = ImGui::CalcTextSize(text.c_str());
@@ -59,12 +59,12 @@ void Render::DrawReceivingScreenText(
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str()); ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str());
} }
void Render::CloseTab(decltype(client_properties_)::iterator& it) { void GuiApplication::CloseTab(decltype(remote_sessions_)::iterator &it) {
// std::unique_lock lock(client_properties_mutex_); // std::unique_lock lock(remote_sessions_mutex_);
if (it != client_properties_.end()) { if (it != remote_sessions_.end()) {
CleanupPeer(it->second); CloseRemoteSession(it->second);
it = client_properties_.erase(it); it = remote_sessions_.erase(it);
if (client_properties_.empty()) { if (remote_sessions_.empty()) {
SDL_Event event; SDL_Event event;
event.type = SDL_EVENT_QUIT; event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event); SDL_PushEvent(&event);
@@ -72,7 +72,7 @@ void Render::CloseTab(decltype(client_properties_)::iterator& it) {
} }
} }
int Render::StreamWindow() { int GuiApplication::StreamWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_), ImVec2(0, fullscreen_button_pressed_ ? 0 : title_bar_height_),
ImGuiCond_Always); ImGuiCond_Always);
@@ -117,22 +117,22 @@ int Render::StreamWindow() {
ImGuiTabBarFlags_AutoSelectNewTabs)) { ImGuiTabBarFlags_AutoSelectNewTabs)) {
is_tab_bar_hovered_ = ImGui::IsWindowHovered(); is_tab_bar_hovered_ = ImGui::IsWindowHovered();
// std::shared_lock lock(client_properties_mutex_); // std::shared_lock lock(remote_sessions_mutex_);
for (auto it = client_properties_.begin(); for (auto it = remote_sessions_.begin();
it != client_properties_.end();) { it != remote_sessions_.end();) {
auto& props = it->second; auto &props = it->second;
if (!props->tab_opened_) { if (!props->tab_opened_) {
std::string remote_id_to_close = props->remote_id_; std::string remote_id_to_close = props->remote_id_;
// lock.unlock(); // lock.unlock();
{ {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(remote_sessions_mutex_);
auto close_it = client_properties_.find(remote_id_to_close); auto close_it = remote_sessions_.find(remote_id_to_close);
if (close_it != client_properties_.end()) { if (close_it != remote_sessions_.end()) {
CloseTab(close_it); CloseTab(close_it);
} }
} }
// lock.lock(); // lock.lock();
it = client_properties_.begin(); it = remote_sessions_.begin();
continue; continue;
} }
@@ -180,13 +180,13 @@ int Render::StreamWindow() {
std::string remote_id_to_erase = props->remote_id_; std::string remote_id_to_erase = props->remote_id_;
// lock.unlock(); // lock.unlock();
{ {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(remote_sessions_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase); auto erase_it = remote_sessions_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) { if (erase_it != remote_sessions_.end()) {
// Ensure we flush pending STREAM_REFRESH_EVENT events and // Ensure we flush pending STREAM_REFRESH_EVENT events and
// clean up peer resources before erasing the entry, otherwise // clean up peer resources before erasing the entry, otherwise
// SDL events may still hold raw pointers to freed // 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. // leading to std::system_error when locking.
CloseTab(erase_it); CloseTab(erase_it);
} }
@@ -194,7 +194,7 @@ int Render::StreamWindow() {
// lock.lock(); // lock.lock();
ImGui::End(); ImGui::End();
ImGui::EndTabItem(); ImGui::EndTabItem();
it = client_properties_.begin(); it = remote_sessions_.begin();
continue; continue;
} else { } else {
DrawConnectionStatusText(props); DrawConnectionStatusText(props);
@@ -209,14 +209,14 @@ int Render::StreamWindow() {
std::string remote_id_to_close = props->remote_id_; std::string remote_id_to_close = props->remote_id_;
// lock.unlock(); // lock.unlock();
{ {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(remote_sessions_mutex_);
auto close_it = client_properties_.find(remote_id_to_close); auto close_it = remote_sessions_.find(remote_id_to_close);
if (close_it != client_properties_.end()) { if (close_it != remote_sessions_.end()) {
CloseTab(close_it); CloseTab(close_it);
} }
} }
// lock.lock(); // lock.lock();
it = client_properties_.begin(); it = remote_sessions_.begin();
continue; continue;
} }
++it; ++it;
@@ -226,24 +226,24 @@ int Render::StreamWindow() {
ImGui::EndTabBar(); ImGui::EndTabBar();
} }
ImGui::End(); // End TabBar ImGui::End(); // End TabBar
} else { } else {
// std::shared_lock lock(client_properties_mutex_); // std::shared_lock lock(remote_sessions_mutex_);
for (auto it = client_properties_.begin(); for (auto it = remote_sessions_.begin();
it != client_properties_.end();) { it != remote_sessions_.end();) {
auto& props = it->second; auto &props = it->second;
if (!props->tab_opened_) { if (!props->tab_opened_) {
std::string remote_id_to_close = props->remote_id_; std::string remote_id_to_close = props->remote_id_;
// lock.unlock(); // lock.unlock();
{ {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(remote_sessions_mutex_);
auto close_it = client_properties_.find(remote_id_to_close); auto close_it = remote_sessions_.find(remote_id_to_close);
if (close_it != client_properties_.end()) { if (close_it != remote_sessions_.end()) {
CloseTab(close_it); CloseTab(close_it);
} }
} }
// lock.lock(); // lock.lock();
it = client_properties_.begin(); it = remote_sessions_.begin();
continue; continue;
} }
@@ -284,14 +284,14 @@ int Render::StreamWindow() {
std::string remote_id_to_erase = props->remote_id_; std::string remote_id_to_erase = props->remote_id_;
// lock.unlock(); // lock.unlock();
{ {
// std::unique_lock unique_lock(client_properties_mutex_); // std::unique_lock unique_lock(remote_sessions_mutex_);
auto erase_it = client_properties_.find(remote_id_to_erase); auto erase_it = remote_sessions_.find(remote_id_to_erase);
if (erase_it != client_properties_.end()) { if (erase_it != remote_sessions_.end()) {
CloseTab(erase_it); CloseTab(erase_it);
} }
} }
// lock.lock(); // lock.lock();
it = client_properties_.begin(); it = remote_sessions_.begin();
continue; continue;
} else { } else {
DrawConnectionStatusText(props); DrawConnectionStatusText(props);
@@ -304,8 +304,8 @@ int Render::StreamWindow() {
} }
// UpdateRenderRect(); // UpdateRenderRect();
ImGui::End(); // End VideoBg ImGui::End(); // End VideoBg
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -1,14 +1,14 @@
#include <algorithm> #include <algorithm>
#include <string> #include <string>
#include "application/gui_application.h"
#include "layout.h" #include "layout.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h"
namespace crossdesk { namespace crossdesk {
std::string CleanMarkdown(const std::string& markdown) { std::string CleanMarkdown(const std::string &markdown) {
std::string result = markdown; std::string result = markdown;
// remove # title mark // remove # title mark
@@ -44,15 +44,15 @@ std::string CleanMarkdown(const std::string& markdown) {
pos = 0; pos = 0;
while ((pos = result.find('.', pos)) != std::string::npos) { while ((pos = result.find('.', pos)) != std::string::npos) {
result.replace(pos, 1, ""); result.replace(pos, 1, "");
pos += 1; // Move to next position after the replacement pos += 1; // Move to next position after the replacement
} }
return result; return result;
} }
int Render::UpdateNotificationWindow() { int GuiApplication::UpdateNotificationWindow() {
if (show_update_notification_window_ && update_available_) { if (show_update_notification_window_ && update_available_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport *viewport = ImGui::GetMainViewport();
float update_notification_window_width = title_bar_button_width_ * 10.0f; float update_notification_window_width = title_bar_button_width_ * 10.0f;
float update_notification_window_height = title_bar_button_width_ * 8.0f; float update_notification_window_height = title_bar_button_width_ * 8.0f;
@@ -204,4 +204,4 @@ int Render::UpdateNotificationWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -16,7 +16,11 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
} }
screen_capturer_sck_impl_ = CreateScreenCapturerSck(); screen_capturer_sck_impl_ = CreateScreenCapturerSck();
screen_capturer_sck_impl_->Init(fps, on_data_); const int ret = screen_capturer_sck_impl_->Init(fps, on_data_);
if (ret != 0) {
screen_capturer_sck_impl_.reset();
return ret;
}
return 0; return 0;
} }
@@ -29,8 +33,11 @@ int ScreenCapturerSck::Destroy() {
} }
int ScreenCapturerSck::Start(bool show_cursor) { int ScreenCapturerSck::Start(bool show_cursor) {
screen_capturer_sck_impl_->Start(show_cursor); if (!screen_capturer_sck_impl_) {
return 0; return -1;
}
return screen_capturer_sck_impl_->Start(show_cursor);
} }
int ScreenCapturerSck::Stop() { int ScreenCapturerSck::Stop() {
@@ -16,7 +16,12 @@
#include <IOKit/graphics/IOGraphicsLib.h> #include <IOKit/graphics/IOGraphicsLib.h>
#include <IOSurface/IOSurface.h> #include <IOSurface/IOSurface.h>
#include <ScreenCaptureKit/ScreenCaptureKit.h> #include <ScreenCaptureKit/ScreenCaptureKit.h>
#include <algorithm>
#include <atomic> #include <atomic>
#include <cctype>
#include <cstring>
#include <limits>
#include <map>
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include "display_info.h" #include "display_info.h"
@@ -28,6 +33,15 @@ class ScreenCapturerSckImpl;
static const int kFullDesktopScreenId = -1; static const int kFullDesktopScreenId = -1;
static std::string NSErrorToString(NSError *error) {
if (!error) {
return "";
}
const char *description = [error.localizedDescription UTF8String];
return description ? description : "";
}
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture // The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
// was reported to be broken before macOS 13 - see http://crbug.com/40234870. // was reported to be broken before macOS 13 - see http://crbug.com/40234870.
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were // Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
@@ -78,6 +92,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
std::map<CGDirectDisplayID, int> display_id_map_reverse_; std::map<CGDirectDisplayID, int> display_id_map_reverse_;
std::map<CGDirectDisplayID, std::string> display_id_name_map_; std::map<CGDirectDisplayID, std::string> display_id_name_map_;
unsigned char *nv12_frame_ = nullptr; unsigned char *nv12_frame_ = nullptr;
size_t nv12_frame_size_ = 0;
int width_ = 0; int width_ = 0;
int height_ = 0; int height_ = 0;
int fps_ = 60; int fps_ = 60;
@@ -100,7 +115,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++ // Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++
// object. The helper may outlive this C++ instance, if a completion-handler is passed to // object. The helper may outlive this C++ instance, if a completion-handler is passed to
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes. // ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
SckHelper *__strong helper_; SckHelper *__strong helper_ = nil;
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's // Callback for returning captured frames, or errors, to the caller. Only used on the caller's
// thread. // thread.
cb_desktop_data _on_data = nullptr; cb_desktop_data _on_data = nullptr;
@@ -110,7 +125,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
// Guards some variables that may be accessed on different threads. // Guards some variables that may be accessed on different threads.
std::mutex lock_; std::mutex lock_;
// Provides captured desktop frames. // Provides captured desktop frames.
SCStream *__strong stream_; SCStream *__strong stream_ = nil;
// Currently selected display, or 0 if the full desktop is selected. This capturer does not // Currently selected display, or 0 if the full desktop is selected. This capturer does not
// support full-desktop capture, and will fall back to the first display. // support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0; CGDirectDisplayID current_display_ = 0;
@@ -182,6 +197,19 @@ ScreenCapturerSckImpl::ScreenCapturerSckImpl() {
} }
ScreenCapturerSckImpl::~ScreenCapturerSckImpl() { ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
SckHelper *helper_to_release = nil;
{
std::lock_guard<std::mutex> lock(lock_);
if (stream_) {
[stream_ stopCaptureWithCompletionHandler:nil];
stream_ = nil;
}
_on_data = nullptr;
helper_to_release = helper_;
helper_ = nil;
}
[helper_to_release releaseCapturer];
display_info_list_.clear(); display_info_list_.clear();
display_id_map_.clear(); display_id_map_.clear();
display_id_map_reverse_.clear(); display_id_map_reverse_.clear();
@@ -190,15 +218,22 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
if (nv12_frame_) { if (nv12_frame_) {
delete[] nv12_frame_; delete[] nv12_frame_;
nv12_frame_ = nullptr; nv12_frame_ = nullptr;
nv12_frame_size_ = 0;
} }
[stream_ stopCaptureWithCompletionHandler:nil];
[helper_ releaseCapturer];
} }
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) { int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
if (!cb) {
LOG_ERROR("Screen capturer callback is null");
return -1;
}
_on_data = cb; _on_data = cb;
fps_ = fps; fps_ = fps > 0 ? fps : 60;
display_info_list_.clear();
display_id_map_.clear();
display_id_map_reverse_.clear();
display_id_name_map_.clear();
if (@available(macOS 10.15, *)) { if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess(); bool has_permission = CGPreflightScreenCaptureAccess();
@@ -216,8 +251,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) { getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
if (error) { if (error) {
capture_error = error; capture_error = error;
LOG_ERROR("Failed to get shareable content: {}", LOG_ERROR("Failed to get shareable content: {}", NSErrorToString(error));
std::string([error.localizedDescription UTF8String]));
} else { } else {
content = result; content = result;
} }
@@ -227,7 +261,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
if (capture_error || !content || content.displays.count == 0) { if (capture_error || !content || content.displays.count == 0) {
LOG_ERROR("Failed to get display info, error: {}", LOG_ERROR("Failed to get display info, error: {}",
std::string([capture_error.localizedDescription UTF8String])); NSErrorToString(capture_error));
return -1; return -1;
} }
@@ -284,51 +318,58 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
} }
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) { int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
if (stream_) { auto display_it = display_id_map_.find(monitor_index);
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) { if (display_it == display_id_map_.end()) {
std::lock_guard<std::mutex> lock(lock_); LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
stream_ = nil; monitor_index, display_id_map_.size());
current_display_ = display_id_map_[monitor_index]; return -1;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = display_id_map_[monitor_index];
StartOrReconfigureCapturer();
} }
const CGDirectDisplayID target_display = display_it->second;
{
std::lock_guard<std::mutex> lock(lock_);
current_display_ = target_display;
}
StartOrReconfigureCapturer();
return 0; return 0;
} }
int ScreenCapturerSckImpl::ResetToInitialMonitor() { int ScreenCapturerSckImpl::ResetToInitialMonitor() {
int target = initial_monitor_index_; int target = initial_monitor_index_;
if (display_info_list_.empty()) return -1; if (display_info_list_.empty()) return -1;
CGDirectDisplayID target_display = display_id_map_[target]; auto display_it = display_id_map_.find(target);
if (current_display_ == target_display) return 0; if (display_it == display_id_map_.end()) {
if (stream_) { LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) { return -1;
std::lock_guard<std::mutex> lock(lock_);
stream_ = nil;
current_display_ = target_display;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = target_display;
StartOrReconfigureCapturer();
} }
CGDirectDisplayID target_display = display_it->second;
if (current_display_ == target_display) return 0;
{
std::lock_guard<std::mutex> lock(lock_);
current_display_ = target_display;
}
StartOrReconfigureCapturer();
return 0; return 0;
} }
int ScreenCapturerSckImpl::Destroy() { int ScreenCapturerSckImpl::Destroy() {
std::lock_guard<std::mutex> lock(lock_); SckHelper *helper_to_release = nil;
if (stream_) { {
LOG_INFO("Destroying stream"); std::lock_guard<std::mutex> lock(lock_);
[stream_ stopCaptureWithCompletionHandler:nil]; if (stream_) {
stream_ = nil; LOG_INFO("Destroying stream");
[stream_ stopCaptureWithCompletionHandler:nil];
stream_ = nil;
}
current_display_ = 0;
permanent_error_ = false;
_on_data = nullptr;
helper_to_release = helper_;
helper_ = nil;
} }
current_display_ = 0;
permanent_error_ = false; [helper_to_release releaseCapturer];
_on_data = nullptr;
[helper_ releaseCapturer];
helper_ = nil;
return 0; return 0;
} }
@@ -416,7 +457,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
// TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for // TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
// best performance. // best performance.
NSError *add_stream_output_error; NSError *add_stream_output_error = nil;
dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL);
bool add_stream_output_result = [stream_ addStreamOutput:helper_ bool add_stream_output_result = [stream_ addStreamOutput:helper_
type:SCStreamOutputTypeScreen type:SCStreamOutputTypeScreen
@@ -425,7 +466,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
if (!add_stream_output_result) { if (!add_stream_output_result) {
stream_ = nil; stream_ = nil;
LOG_ERROR("addStreamOutput failed"); LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error));
permanent_error_ = true; permanent_error_ = true;
return; return;
} }
@@ -436,7 +477,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
// calls stopCaptureWithCompletionHandler on the stream, which cancels // calls stopCaptureWithCompletionHandler on the stream, which cancels
// this handler. // this handler.
permanent_error_ = true; permanent_error_ = true;
LOG_ERROR("startCaptureWithCompletionHandler failed"); LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error));
} else { } else {
LOG_INFO("Capture started"); LOG_INFO("Capture started");
} }
@@ -448,8 +489,18 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer, void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
CFDictionaryRef attachment) { CFDictionaryRef attachment) {
(void)attachment;
if (!pixelBuffer) {
return;
}
size_t width = CVPixelBufferGetWidth(pixelBuffer); size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(pixelBuffer); size_t height = CVPixelBufferGetHeight(pixelBuffer);
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
CVPixelBufferGetPlaneCount(pixelBuffer));
return;
}
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
if (status != kCVReturnSuccess) { if (status != kCVReturnSuccess) {
@@ -458,18 +509,37 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
} }
size_t required_size = width * height * 3 / 2; size_t required_size = width * height * 3 / 2;
if (!nv12_frame_ || (width_ * height_ * 3 / 2 < required_size)) { if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
std::lock_guard<std::mutex> lock(lock_);
if (!_on_data) {
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
delete[] nv12_frame_; delete[] nv12_frame_;
nv12_frame_ = new unsigned char[required_size]; nv12_frame_ = new unsigned char[required_size];
width_ = width; nv12_frame_size_ = required_size;
height_ = height;
} }
width_ = static_cast<int>(width);
height_ = static_cast<int>(height);
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
return;
}
unsigned char *dst_y = nv12_frame_; unsigned char *dst_y = nv12_frame_;
for (size_t row = 0; row < height; ++row) { for (size_t row = 0; row < height; ++row) {
@@ -481,7 +551,8 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width); memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
} }
_on_data(nv12_frame_, width * height * 3 / 2, width, height, _on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
static_cast<int>(height),
display_id_name_map_[current_display_].c_str()); display_id_name_map_[current_display_].c_str());
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
@@ -503,10 +574,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
} }
SckHelper *local_helper = helper_; SckHelper *local_helper = helper_;
if (!local_helper) {
LOG_ERROR("Cannot reconfigure capturer: helper is null");
return;
}
auto handler = ^(SCShareableContent *content, NSError *error) { auto handler = ^(SCShareableContent *content, NSError *error) {
if (error) { if (error) {
LOG_ERROR("getShareableContent failed: {}", LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error));
std::string([error.localizedDescription UTF8String]));
[local_helper onShareableContentCreated:nil]; [local_helper onShareableContentCreated:nil];
return; return;
} }
@@ -111,6 +111,7 @@ int ScreenCapturerDxgi::Resume(int monitor_index) {
} }
int ScreenCapturerDxgi::SwitchTo(int monitor_index) { int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) { if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index); LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1; return -1;
@@ -121,6 +122,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
if (!CreateDuplicationForMonitor(monitor_index_)) { if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}", LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load()); monitor_index_.load());
paused_ = false; // Reset paused_ on failure
return -2; return -2;
} }
paused_ = false; paused_ = false;
@@ -130,6 +132,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
} }
int ScreenCapturerDxgi::ResetToInitialMonitor() { int ScreenCapturerDxgi::ResetToInitialMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (display_info_list_.empty()) return -1; if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_; int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1; if (target < 0 || target >= (int)display_info_list_.size()) return -1;
@@ -245,6 +248,7 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
} }
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() { bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
ReleaseDuplication(); ReleaseDuplication();
int current_monitor = monitor_index_.load(); int current_monitor = monitor_index_.load();
if (CreateDuplicationForMonitor(current_monitor)) { if (CreateDuplicationForMonitor(current_monitor)) {
@@ -374,8 +378,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
even_width, even_width, even_height); even_width, even_width, even_height);
if (callback_) { if (callback_) {
callback_(nv12_frame_, nv12_size, even_width, even_height, int idx = monitor_index_.load();
display_info_list_[monitor_index_].name.c_str()); if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[idx].name.c_str());
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
}
} }
d3d_context_->Unmap(staging_.Get(), 0); d3d_context_->Unmap(staging_.Get(), 0);
@@ -72,6 +72,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
std::thread thread_; std::thread thread_;
int fps_ = 60; int fps_ = 60;
cb_desktop_data callback_ = nullptr; cb_desktop_data callback_ = nullptr;
std::mutex switch_mutex_;
unsigned char* nv12_frame_ = nullptr; unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0; int nv12_width_ = 0;
@@ -148,7 +148,14 @@ void ScreenCapturerGdi::CaptureLoop() {
continue; continue;
} }
const auto& di = display_info_list_[monitor_index_]; int idx = monitor_index_.load();
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
const auto& di = display_info_list_[idx];
int left = di.left; int left = di.left;
int top = di.top; int top = di.top;
int width = di.width & ~1; int width = di.width & ~1;
@@ -306,7 +306,7 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
return 0; return 0;
} }
if (monitor_index >= display_info_list_.size()) { if (monitor_index < 0 || monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid monitor index: {}", monitor_index); LOG_ERROR("Invalid monitor index: {}", monitor_index);
return -1; return -1;
} }
@@ -46,6 +46,7 @@ struct SecureDesktopServiceStatus {
DWORD active_session_id = 0xFFFFFFFF; DWORD active_session_id = 0xFFFFFFFF;
DWORD error_code = 0; DWORD error_code = 0;
std::string interactive_stage; std::string interactive_stage;
std::string interactive_desktop;
std::string error; std::string error;
}; };
@@ -134,12 +135,16 @@ class WgcPluginCapturer final : public ScreenCapturer {
std::string BuildSecureCaptureCommand(int left, int top, int width, int height, std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
bool show_cursor, bool show_cursor,
const std::string& stage) { const std::string& stage,
const std::string& desktop) {
std::ostringstream stream; std::ostringstream stream;
stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0); << ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0);
if (!stage.empty()) { if (!stage.empty()) {
stream << ":" << stage; stream << ":" << stage;
if (!desktop.empty()) {
stream << ":" << desktop;
}
} }
return stream.str(); 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, std::string BuildSecureCaptureStartCommand(int left, int top, int width,
int height, bool show_cursor, int height, bool show_cursor,
int fps, int fps,
const std::string& stage) { const std::string& stage,
const std::string& desktop) {
std::ostringstream stream; std::ostringstream stream;
stream << kCrossDeskSecureInputCaptureStartCommandPrefix << left << ":" << top stream << kCrossDeskSecureInputCaptureStartCommandPrefix << left << ":" << top
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0) << ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0)
<< ":" << fps; << ":" << fps;
if (!stage.empty()) { if (!stage.empty()) {
stream << ":" << stage; stream << ":" << stage;
if (!desktop.empty()) {
stream << ":" << desktop;
}
} }
return stream.str(); return stream.str();
} }
@@ -284,6 +293,7 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
json.value("last_session_event", std::string()))) { json.value("last_session_event", std::string()))) {
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu); status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status->interactive_stage = "user-desktop"; status->interactive_stage = "user-desktop";
status->interactive_desktop.clear();
status->capture_active = false; status->capture_active = false;
return true; return true;
} }
@@ -291,6 +301,8 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu); status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status->helper_running = json.value("secure_input_helper_running", false); status->helper_running = json.value("secure_input_helper_running", false);
status->interactive_stage = json.value("interactive_stage", std::string()); status->interactive_stage = json.value("interactive_stage", std::string());
status->interactive_desktop =
json.value("interactive_input_desktop", std::string());
const bool secure_desktop_active = const bool secure_desktop_active =
json.value("interactive_secure_desktop_active", json.value("interactive_secure_desktop_active",
json.value("secure_desktop_active", false)); json.value("secure_desktop_active", false));
@@ -358,6 +370,7 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top, bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
int width, int height, bool show_cursor, int width, int height, bool show_cursor,
const std::string& stage, const std::string& stage,
const std::string& desktop,
std::vector<uint8_t>* nv12_frame_out, std::vector<uint8_t>* nv12_frame_out,
int* captured_width_out, int* captured_width_out,
int* captured_height_out, int* captured_height_out,
@@ -368,7 +381,8 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
} }
const std::string command = 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; std::vector<uint8_t> response;
if (!QuerySecureDesktopHelperCommand(session_id, command, &response, if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
error_out)) { error_out)) {
@@ -823,6 +837,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
secure_shared_fps_ = 0; secure_shared_fps_ = 0;
secure_shared_show_cursor_ = true; secure_shared_show_cursor_ = true;
secure_shared_stage_.clear(); secure_shared_stage_.clear();
secure_shared_desktop_.clear();
} }
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id, bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
@@ -952,7 +967,8 @@ bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
bool ScreenCapturerWin::StartSecureDesktopSharedCapture( bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
DWORD session_id, int left, int top, int width, int height, 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) { std::string* error_out) {
const size_t payload_size = static_cast<size_t>(width) * height * 3 / 2; const size_t payload_size = static_cast<size_t>(width) * height * 3 / 2;
const size_t mapping_size = const size_t mapping_size =
@@ -968,7 +984,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
secure_shared_session_id_ == session_id && secure_shared_session_id_ == session_id &&
secure_shared_left_ == left && secure_shared_top_ == top && secure_shared_left_ == left && secure_shared_top_ == top &&
secure_shared_width_ == width && secure_shared_height_ == height && 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 && secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps &&
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) { OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
return true; return true;
@@ -978,7 +994,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
const std::string command = const std::string command =
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps, BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
stage); stage, desktop);
std::vector<uint8_t> response; std::vector<uint8_t> response;
if (!QuerySecureDesktopHelperCommand(session_id, command, &response, if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
error_out)) { error_out)) {
@@ -1002,6 +1018,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
secure_shared_show_cursor_ = show_cursor; secure_shared_show_cursor_ = show_cursor;
secure_shared_fps_ = fps; secure_shared_fps_ = fps;
secure_shared_stage_ = stage; secure_shared_stage_ = stage;
secure_shared_desktop_ = desktop;
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) { if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
StopSecureDesktopSharedCapture(session_id); StopSecureDesktopSharedCapture(session_id);
@@ -1161,8 +1178,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top, if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
width, height, width, height,
status.interactive_stage, show_cursor, status.interactive_stage,
shared_fps, &error_message) && status.interactive_desktop,
show_cursor, shared_fps,
&error_message) &&
ReadSecureDesktopSharedFrame( ReadSecureDesktopSharedFrame(
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame, static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
&captured_width, &captured_height, &error_message)) { &captured_width, &captured_height, &error_message)) {
@@ -1177,6 +1196,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
QuerySecureDesktopHelperFrame(status.active_session_id, left, top, QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
width, height, show_cursor, width, height, show_cursor,
status.interactive_stage, status.interactive_stage,
status.interactive_desktop,
&secure_frame, &captured_width, &secure_frame, &captured_width,
&captured_height, &error_message)) { &captured_height, &error_message)) {
if (cb_orig_ && !secure_frame.empty()) { if (cb_orig_ && !secure_frame.empty()) {
@@ -75,6 +75,7 @@ class ScreenCapturerWin : public ScreenCapturer {
int secure_shared_fps_ = 0; int secure_shared_fps_ = 0;
bool secure_shared_show_cursor_ = true; bool secure_shared_show_cursor_ = true;
std::string secure_shared_stage_; std::string secure_shared_stage_;
std::string secure_shared_desktop_;
bool secure_shared_capture_started_ = false; bool secure_shared_capture_started_ = false;
void BuildCanonicalFromImpl(); void BuildCanonicalFromImpl();
@@ -87,6 +88,7 @@ class ScreenCapturerWin : public ScreenCapturer {
bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top, bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top,
int width, int height, int width, int height,
const std::string& stage, const std::string& stage,
const std::string& desktop,
bool show_cursor, int fps, bool show_cursor, int fps,
std::string* error_out); std::string* error_out);
void StopSecureDesktopSharedCapture(DWORD session_id); void StopSecureDesktopSharedCapture(DWORD session_id);
+136 -21
View File
@@ -64,6 +64,7 @@ struct ScopedEnvironmentBlock {
LPVOID environment = nullptr; LPVOID environment = nullptr;
}; };
std::wstring Utf8ToWide(const std::string& value);
std::string WideToUtf8(const std::wstring& value); std::string WideToUtf8(const std::wstring& value);
std::wstring GetCurrentExecutablePathW() { std::wstring GetCurrentExecutablePathW() {
@@ -358,24 +359,32 @@ std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel,
std::string BuildSecureInputHelperKeyboardCommand( std::string BuildSecureInputHelperKeyboardCommand(
int key_code, bool is_down, uint32_t scan_code, bool extended, 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; std::ostringstream stream;
stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":" stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":"
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0); << (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
if (!interactive_stage.empty()) { if (!interactive_stage.empty()) {
stream << ":" << interactive_stage; stream << ":" << interactive_stage;
if (!interactive_desktop.empty()) {
stream << ":" << interactive_desktop;
}
} }
return stream.str(); return stream.str();
} }
std::string BuildSecureInputHelperMouseCommand( std::string BuildSecureInputHelperMouseCommand(
int x, int y, int wheel, int flag, 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; std::ostringstream stream;
stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":" stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
<< wheel << ":" << flag; << wheel << ":" << flag;
if (!interactive_stage.empty()) { if (!interactive_stage.empty()) {
stream << ":" << interactive_stage; stream << ":" << interactive_stage;
if (!interactive_desktop.empty()) {
stream << ":" << interactive_desktop;
}
} }
return stream.str(); return stream.str();
} }
@@ -592,15 +601,23 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
} }
bool IsCredentialUiVisible(bool prelogin, bool session_locked, bool IsCredentialUiVisible(bool prelogin, bool session_locked,
bool logon_ui_running, bool logon_ui_running, bool consent_ui_visible,
bool input_desktop_available, bool input_desktop_available,
bool secure_desktop_active) { bool secure_desktop_active) {
return (prelogin || session_locked || secure_desktop_active) && return consent_ui_visible ||
(logon_ui_running || !input_desktop_available); ((prelogin || session_locked || secure_desktop_active) &&
(logon_ui_running || !input_desktop_available));
} }
std::wstring SecureInputHelperDesktopForStage( 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" || if (interactive_stage == "credential-ui" ||
interactive_stage == "secure-desktop") { interactive_stage == "secure-desktop") {
return L"winsta0\\Winlogon"; return L"winsta0\\Winlogon";
@@ -663,7 +680,12 @@ bool QuerySessionLockState(DWORD session_id, bool* session_locked_out) {
return success; 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); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) { if (snapshot == INVALID_HANDLE_VALUE) {
return false; return false;
@@ -674,7 +696,7 @@ bool IsLogonUiRunningInSession(DWORD session_id) {
bool found = false; bool found = false;
if (Process32FirstW(snapshot, &entry)) { if (Process32FirstW(snapshot, &entry)) {
do { do {
if (_wcsicmp(entry.szExeFile, L"LogonUI.exe") != 0) { if (_wcsicmp(entry.szExeFile, executable_name) != 0) {
continue; continue;
} }
@@ -691,6 +713,14 @@ bool IsLogonUiRunningInSession(DWORD session_id) {
return found; 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 GetInputDesktopInfo() {
InputDesktopInfo info; InputDesktopInfo info;
HDESK desktop = OpenInputDesktop(0, FALSE, GENERIC_READ); HDESK desktop = OpenInputDesktop(0, FALSE, GENERIC_READ);
@@ -724,6 +754,24 @@ InputDesktopInfo GetInputDesktopInfo() {
return info; 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) { std::string WideToUtf8(const std::wstring& value) {
if (value.empty()) { if (value.empty()) {
return {}; return {};
@@ -1024,6 +1072,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
secure_input_helper_last_error_code_ = 0; secure_input_helper_last_error_code_ = 0;
session_locked_ = false; session_locked_ = false;
logon_ui_visible_ = false; logon_ui_visible_ = false;
consent_ui_visible_ = false;
prelogin_ = false; prelogin_ = false;
secure_desktop_active_ = false; secure_desktop_active_ = false;
input_desktop_available_ = false; input_desktop_available_ = false;
@@ -1032,6 +1081,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
session_helper_report_input_desktop_available_ = false; session_helper_report_input_desktop_available_ = false;
session_helper_report_lock_app_visible_ = false; session_helper_report_lock_app_visible_ = false;
session_helper_report_logon_ui_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_secure_desktop_active_ = false;
session_helper_report_credential_ui_visible_ = false; session_helper_report_credential_ui_visible_ = false;
session_helper_report_unlock_ui_visible_ = false; session_helper_report_unlock_ui_visible_ = false;
@@ -1056,6 +1106,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
session_helper_report_interactive_stage_.clear(); session_helper_report_interactive_stage_.clear();
secure_input_helper_last_error_.clear(); secure_input_helper_last_error_.clear();
secure_input_helper_interactive_stage_.clear(); secure_input_helper_interactive_stage_.clear();
secure_input_helper_interactive_desktop_.clear();
last_session_event_type_ = 0; last_session_event_type_ = 0;
last_session_event_session_id_ = active_session_id_; last_session_event_session_id_ = active_session_id_;
RefreshSessionState(); RefreshSessionState();
@@ -1283,6 +1334,7 @@ void CrossDeskServiceHost::RefreshSessionState() {
process_session_id_ = process_session_id; process_session_id_ = process_session_id;
} }
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_); logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
InputDesktopInfo desktop_info = GetInputDesktopInfo(); InputDesktopInfo desktop_info = GetInputDesktopInfo();
input_desktop_available_ = desktop_info.available; input_desktop_available_ = desktop_info.available;
input_desktop_error_code_ = desktop_info.error_code; input_desktop_error_code_ = desktop_info.error_code;
@@ -1312,6 +1364,7 @@ void CrossDeskServiceHost::ResetSessionHelperReportedStateLocked(
session_helper_report_input_desktop_.clear(); session_helper_report_input_desktop_.clear();
session_helper_report_lock_app_visible_ = false; session_helper_report_lock_app_visible_ = false;
session_helper_report_logon_ui_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_secure_desktop_active_ = false;
session_helper_report_credential_ui_visible_ = false; session_helper_report_credential_ui_visible_ = false;
session_helper_report_unlock_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 = const bool service_host_credential_ui_visible =
!session_helper_status_ok_ && !session_helper_status_ok_ &&
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_, IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
consent_ui_visible_,
input_desktop_available_, input_desktop_available_,
secure_desktop_active_); secure_desktop_active_);
return IsSasSecureDesktopGraceActiveLocked() || prelogin_ || return IsSasSecureDesktopGraceActiveLocked() || prelogin_ ||
secure_desktop_active_ || service_host_credential_ui_visible || secure_desktop_active_ || service_host_credential_ui_visible ||
session_helper_report_consent_ui_visible_ ||
session_helper_report_credential_ui_visible_ || session_helper_report_credential_ui_visible_ ||
session_helper_report_secure_desktop_active_ || session_helper_report_secure_desktop_active_ ||
session_helper_report_unlock_ui_visible_ || session_helper_report_unlock_ui_visible_ ||
@@ -1392,6 +1447,7 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
const bool service_host_credential_ui_visible = const bool service_host_credential_ui_visible =
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_, IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
consent_ui_visible_,
input_desktop_available_, input_desktop_available_,
secure_desktop_active_); secure_desktop_active_);
return DetermineInteractiveStage( return DetermineInteractiveStage(
@@ -1401,6 +1457,33 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
session_helper_report_secure_desktop_active_ || secure_desktop_active_); 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 CrossDeskServiceHost::GetSessionHelperPath() const {
std::wstring current_executable = GetCurrentExecutablePathW(); std::wstring current_executable = GetCurrentExecutablePathW();
if (current_executable.empty()) { if (current_executable.empty()) {
@@ -1491,6 +1574,7 @@ void CrossDeskServiceHost::ReapSecureInputHelper() {
secure_input_helper_exit_code_ = exit_code; secure_input_helper_exit_code_ = exit_code;
secure_input_helper_started_at_tick_ = 0; secure_input_helper_started_at_tick_ = 0;
secure_input_helper_interactive_stage_.clear(); secure_input_helper_interactive_stage_.clear();
secure_input_helper_interactive_desktop_.clear();
} }
if (process_handle != nullptr) { if (process_handle != nullptr) {
@@ -1550,6 +1634,7 @@ void CrossDeskServiceHost::StopSecureInputHelper() {
secure_input_helper_process_id_ = 0; secure_input_helper_process_id_ = 0;
secure_input_helper_started_at_tick_ = 0; secure_input_helper_started_at_tick_ = 0;
secure_input_helper_interactive_stage_.clear(); secure_input_helper_interactive_stage_.clear();
secure_input_helper_interactive_desktop_.clear();
} }
if (stop_event_handle != nullptr) { if (stop_event_handle != nullptr) {
@@ -1678,7 +1763,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
} }
bool CrossDeskServiceHost::LaunchSecureInputHelper( 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(); std::wstring helper_path = GetSecureInputHelperPath();
if (helper_path.empty() || !std::filesystem::exists(helper_path)) { if (helper_path.empty() || !std::filesystem::exists(helper_path)) {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
@@ -1713,7 +1799,7 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(
STARTUPINFOW startup_info{}; STARTUPINFOW startup_info{};
startup_info.cb = sizeof(startup_info); startup_info.cb = sizeof(startup_info);
std::wstring secure_input_helper_desktop = std::wstring secure_input_helper_desktop =
SecureInputHelperDesktopForStage(interactive_stage); SecureInputHelperDesktopForStage(interactive_stage, interactive_desktop);
startup_info.lpDesktop = startup_info.lpDesktop =
const_cast<LPWSTR>(secure_input_helper_desktop.c_str()); const_cast<LPWSTR>(secure_input_helper_desktop.c_str());
PROCESS_INFORMATION process_info{}; PROCESS_INFORMATION process_info{};
@@ -1765,13 +1851,14 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(
secure_input_helper_running_ = true; secure_input_helper_running_ = true;
secure_input_helper_started_at_tick_ = GetTickCount64(); secure_input_helper_started_at_tick_ = GetTickCount64();
secure_input_helper_interactive_stage_ = interactive_stage; secure_input_helper_interactive_stage_ = interactive_stage;
secure_input_helper_interactive_desktop_ = interactive_desktop;
} }
LOG_INFO( LOG_INFO(
"Secure input helper started: session_id={}, pid={}, stage='{}', " "Secure input helper started: session_id={}, pid={}, stage='{}', "
"desktop='{}'", "interactive_desktop='{}', desktop='{}'",
session_id, process_info.dwProcessId, interactive_stage, session_id, process_info.dwProcessId, interactive_stage,
WideToUtf8(secure_input_helper_desktop)); interactive_desktop, WideToUtf8(secure_input_helper_desktop));
return true; return true;
} }
@@ -1860,6 +1947,8 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
json.value("lock_app_visible", false); json.value("lock_app_visible", false);
session_helper_report_logon_ui_visible_ = session_helper_report_logon_ui_visible_ =
json.value("logon_ui_visible", false); json.value("logon_ui_visible", false);
session_helper_report_consent_ui_visible_ =
json.value("consent_ui_visible", false);
session_helper_report_secure_desktop_active_ = session_helper_report_secure_desktop_active_ =
json.value("secure_desktop_active", false); json.value("secure_desktop_active", false);
session_helper_report_credential_ui_visible_ = session_helper_report_credential_ui_visible_ =
@@ -1885,6 +1974,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
process_session_id_ = process_session_id; process_session_id_ = process_session_id;
} }
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_); logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
InputDesktopInfo desktop_info = GetInputDesktopInfo(); InputDesktopInfo desktop_info = GetInputDesktopInfo();
input_desktop_available_ = desktop_info.available; input_desktop_available_ = desktop_info.available;
input_desktop_error_code_ = desktop_info.error_code; 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={}", LOG_INFO("Session event: type={}, session_id={}, active_session_id={}",
SessionEventToString(event_type), session_id, active_session_id_); SessionEventToString(event_type), session_id, active_session_id_);
EnsureSessionHelper(); EnsureSessionHelper();
if (!secure_desktop_active_ && !logon_ui_visible_) { if (!secure_desktop_active_ && !logon_ui_visible_ && !consent_ui_visible_) {
StopSecureInputHelper(); StopSecureInputHelper();
} }
} }
@@ -1955,10 +2045,13 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
bool launch_secure_input_helper = false; bool launch_secure_input_helper = false;
DWORD secure_input_target_session_id = 0xFFFFFFFF; DWORD secure_input_target_session_id = 0xFFFFFFFF;
std::string secure_input_interactive_stage; std::string secure_input_interactive_stage;
std::string secure_input_interactive_desktop;
{ {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
secure_input_target_session_id = active_session_id_; secure_input_target_session_id = active_session_id_;
secure_input_interactive_stage = ResolveInteractiveStageLocked(); secure_input_interactive_stage = ResolveInteractiveStageLocked();
secure_input_interactive_desktop =
ResolveInteractiveDesktopLocked(secure_input_interactive_stage);
keep_secure_input_helper = keep_secure_input_helper =
ShouldKeepSecureInputHelperLocked(secure_input_target_session_id); ShouldKeepSecureInputHelperLocked(secure_input_target_session_id);
launch_secure_input_helper = launch_secure_input_helper =
@@ -1966,14 +2059,17 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
(!secure_input_helper_running_ || (!secure_input_helper_running_ ||
secure_input_helper_session_id_ != secure_input_target_session_id || secure_input_helper_session_id_ != secure_input_target_session_id ||
secure_input_helper_interactive_stage_ != 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 (keep_secure_input_helper) {
if (launch_secure_input_helper) { if (launch_secure_input_helper) {
StopSecureInputHelper(); StopSecureInputHelper();
LaunchSecureInputHelper(secure_input_target_session_id, LaunchSecureInputHelper(secure_input_target_session_id,
secure_input_interactive_stage); secure_input_interactive_stage,
secure_input_interactive_desktop);
} }
} else { } else {
StopSecureInputHelper(); StopSecureInputHelper();
@@ -1999,6 +2095,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
EscapeJsonString(secure_input_helper_last_error_); EscapeJsonString(secure_input_helper_last_error_);
std::string secure_input_helper_interactive_stage = std::string secure_input_helper_interactive_stage =
EscapeJsonString(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_; bool interactive_state_ready = session_helper_status_ok_;
const bool sas_secure_desktop_grace_active = const bool sas_secure_desktop_grace_active =
IsSasSecureDesktopGraceActiveLocked(); IsSasSecureDesktopGraceActiveLocked();
@@ -2011,6 +2109,7 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
: false; : false;
const bool service_host_credential_ui_visible = const bool service_host_credential_ui_visible =
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_, IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
consent_ui_visible_,
input_desktop_available_, input_desktop_available_,
secure_desktop_active_); secure_desktop_active_);
bool credential_ui_visible = bool credential_ui_visible =
@@ -2062,6 +2161,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
<< ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false") << ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
<< ",\"credential_ui_visible\":" << ",\"credential_ui_visible\":"
<< (credential_ui_visible ? "true" : "false") << (credential_ui_visible ? "true" : "false")
<< ",\"consent_ui_visible\":"
<< (consent_ui_visible_ ? "true" : "false")
<< ",\"password_box_visible\":" << ",\"password_box_visible\":"
<< (credential_ui_visible ? "true" : "false") << (credential_ui_visible ? "true" : "false")
<< ",\"logon_ui_visible\":" << (logon_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_lock_app_visible_ ? "true" : "false")
<< ",\"session_helper_report_logon_ui_visible\":" << ",\"session_helper_report_logon_ui_visible\":"
<< (session_helper_report_logon_ui_visible_ ? "true" : "false") << (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\":"
<< (session_helper_report_secure_desktop_active_ ? "true" : "false") << (session_helper_report_secure_desktop_active_ ? "true" : "false")
<< ",\"session_helper_report_credential_ui_visible\":" << ",\"session_helper_report_credential_ui_visible\":"
@@ -2134,6 +2237,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
<< secure_input_helper_last_error_code_ << secure_input_helper_last_error_code_
<< ",\"secure_input_helper_stage\":\"" << ",\"secure_input_helper_stage\":\""
<< secure_input_helper_interactive_stage << "\"" << secure_input_helper_interactive_stage << "\""
<< ",\"secure_input_helper_desktop\":\""
<< secure_input_helper_interactive_desktop << "\""
<< ",\"secure_input_helper_uptime_ms\":" << ",\"secure_input_helper_uptime_ms\":"
<< (secure_input_helper_started_at_tick_ >= started_at_tick_ << (secure_input_helper_started_at_tick_ >= started_at_tick_
? (GetTickCount64() - secure_input_helper_started_at_tick_) ? (GetTickCount64() - secure_input_helper_started_at_tick_)
@@ -2190,12 +2295,15 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
bool helper_running = false; bool helper_running = false;
bool can_inject = false; bool can_inject = false;
std::string interactive_stage; std::string interactive_stage;
std::string interactive_desktop;
{ {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = active_session_id_; target_session_id = active_session_id_;
interactive_stage = ResolveInteractiveStageLocked(); interactive_stage = ResolveInteractiveStageLocked();
interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage);
const bool helper_stage_matches = 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_ && helper_running = secure_input_helper_running_ &&
secure_input_helper_session_id_ == target_session_id && secure_input_helper_session_id_ == target_session_id &&
helper_stage_matches; helper_stage_matches;
@@ -2211,7 +2319,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
if (!helper_running) { if (!helper_running) {
StopSecureInputHelper(); 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_); std::lock_guard<std::mutex> lock(state_mutex_);
return BuildErrorJson(secure_input_helper_last_error_.c_str(), return BuildErrorJson(secure_input_helper_last_error_.c_str(),
secure_input_helper_last_error_code_); secure_input_helper_last_error_code_);
@@ -2221,7 +2330,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
return QueryNamedPipeMessage( return QueryNamedPipeMessage(
GetCrossDeskSecureInputHelperPipeName(target_session_id), GetCrossDeskSecureInputHelperPipeName(target_session_id),
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code, BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
extended, interactive_stage), extended, interactive_stage,
interactive_desktop),
1000); 1000);
} }
@@ -2237,12 +2347,15 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
bool helper_running = false; bool helper_running = false;
bool can_inject = false; bool can_inject = false;
std::string interactive_stage; std::string interactive_stage;
std::string interactive_desktop;
{ {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = active_session_id_; target_session_id = active_session_id_;
interactive_stage = ResolveInteractiveStageLocked(); interactive_stage = ResolveInteractiveStageLocked();
interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage);
const bool helper_stage_matches = 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_ && helper_running = secure_input_helper_running_ &&
secure_input_helper_session_id_ == target_session_id && secure_input_helper_session_id_ == target_session_id &&
helper_stage_matches; helper_stage_matches;
@@ -2258,7 +2371,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
if (!helper_running) { if (!helper_running) {
StopSecureInputHelper(); 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_); std::lock_guard<std::mutex> lock(state_mutex_);
return BuildErrorJson(secure_input_helper_last_error_.c_str(), return BuildErrorJson(secure_input_helper_last_error_.c_str(),
secure_input_helper_last_error_code_); secure_input_helper_last_error_code_);
@@ -2267,7 +2381,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
return QueryNamedPipeMessage( return QueryNamedPipeMessage(
GetCrossDeskSecureInputHelperPipeName(target_session_id), GetCrossDeskSecureInputHelperPipeName(target_session_id),
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage), BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage,
interactive_desktop),
1000); 1000);
} }
+7 -1
View File
@@ -46,7 +46,8 @@ class CrossDeskServiceHost {
void ReapSecureInputHelper(); void ReapSecureInputHelper();
void StopSecureInputHelper(); void StopSecureInputHelper();
bool LaunchSecureInputHelper(DWORD session_id, 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 GetSessionHelperPath() const;
std::wstring GetSessionHelperStopEventName(DWORD session_id) const; std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
std::wstring GetSecureInputHelperPath() const; std::wstring GetSecureInputHelperPath() const;
@@ -60,6 +61,8 @@ class CrossDeskServiceHost {
bool IsSasSecureDesktopGraceActiveLocked() const; bool IsSasSecureDesktopGraceActiveLocked() const;
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const; bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
std::string ResolveInteractiveStageLocked() const; std::string ResolveInteractiveStageLocked() const;
std::string ResolveInteractiveDesktopLocked(
const std::string& interactive_stage) const;
void RefreshSessionHelperReportedState(); void RefreshSessionHelperReportedState();
void RecordSessionEvent(DWORD event_type, DWORD session_id); void RecordSessionEvent(DWORD event_type, DWORD session_id);
std::string HandleIpcCommand(const std::string& command); std::string HandleIpcCommand(const std::string& command);
@@ -108,6 +111,7 @@ class CrossDeskServiceHost {
ULONGLONG sas_secure_desktop_until_tick_ = 0; ULONGLONG sas_secure_desktop_until_tick_ = 0;
bool session_locked_ = false; bool session_locked_ = false;
bool logon_ui_visible_ = false; bool logon_ui_visible_ = false;
bool consent_ui_visible_ = false;
bool prelogin_ = false; bool prelogin_ = false;
bool secure_desktop_active_ = false; bool secure_desktop_active_ = false;
bool input_desktop_available_ = false; bool input_desktop_available_ = false;
@@ -117,6 +121,7 @@ class CrossDeskServiceHost {
bool session_helper_report_input_desktop_available_ = false; bool session_helper_report_input_desktop_available_ = false;
bool session_helper_report_lock_app_visible_ = false; bool session_helper_report_lock_app_visible_ = false;
bool session_helper_report_logon_ui_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_secure_desktop_active_ = false;
bool session_helper_report_credential_ui_visible_ = false; bool session_helper_report_credential_ui_visible_ = false;
bool session_helper_report_unlock_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 session_helper_report_interactive_stage_;
std::string secure_input_helper_last_error_; std::string secure_input_helper_last_error_;
std::string secure_input_helper_interactive_stage_; std::string secure_input_helper_interactive_stage_;
std::string secure_input_helper_interactive_desktop_;
static CrossDeskServiceHost* instance_; static CrossDeskServiceHost* instance_;
}; };
+106 -30
View File
@@ -51,6 +51,7 @@ struct HelperState {
std::string input_desktop_name; std::string input_desktop_name;
bool lock_app_visible = false; bool lock_app_visible = false;
bool logon_ui_visible = false; bool logon_ui_visible = false;
bool consent_ui_visible = false;
bool secure_desktop_active = false; bool secure_desktop_active = false;
ULONGLONG started_at_tick = 0; ULONGLONG started_at_tick = 0;
ULONGLONG last_update_tick = 0; ULONGLONG last_update_tick = 0;
@@ -64,6 +65,7 @@ struct SecureCaptureRequest {
bool show_cursor = true; bool show_cursor = true;
int fps = 30; int fps = 30;
std::string interactive_stage; std::string interactive_stage;
std::string interactive_desktop;
}; };
struct SecureMouseRequest { struct SecureMouseRequest {
@@ -72,6 +74,7 @@ struct SecureMouseRequest {
int wheel = 0; int wheel = 0;
int flag = 0; int flag = 0;
std::string interactive_stage; std::string interactive_stage;
std::string interactive_desktop;
}; };
struct SecureCaptureBuffers { struct SecureCaptureBuffers {
@@ -288,6 +291,10 @@ bool IsLogonUiRunningInCurrentSession(DWORD session_id) {
return IsProcessRunningInCurrentSession(L"LogonUI.exe", 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) { bool IsLockAppRunningInCurrentSession(DWORD session_id) {
return IsProcessRunningInCurrentSession(L"LockApp.exe", 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 IsCredentialUiVisible(bool session_locked, bool logon_ui_running,
bool consent_ui_visible,
bool input_desktop_available, bool input_desktop_available,
bool secure_desktop_active) { bool secure_desktop_active) {
return (session_locked || secure_desktop_active) && return consent_ui_visible ||
(logon_ui_running || !input_desktop_available); ((session_locked || secure_desktop_active) &&
(logon_ui_running || !input_desktop_available));
} }
std::string BuildErrorJson(const char* error, DWORD error_code = 0) { std::string BuildErrorJson(const char* error, DWORD error_code = 0) {
@@ -367,6 +376,10 @@ void UpdateHelperState(HelperState* helper_state) {
IsLockAppRunningInCurrentSession(helper_state->session_id); IsLockAppRunningInCurrentSession(helper_state->session_id);
bool logon_ui_visible = bool logon_ui_visible =
IsLogonUiRunningInCurrentSession(helper_state->session_id); 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 = const bool input_desktop_is_winlogon =
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0; _stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
const bool inaccessible_secure_input_desktop = 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->input_desktop_name = desktop_info.name;
helper_state->lock_app_visible = lock_app_visible; helper_state->lock_app_visible = lock_app_visible;
helper_state->logon_ui_visible = logon_ui_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->secure_desktop_active = secure_desktop_active;
helper_state->last_update_tick = GetTickCount64(); 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); std::lock_guard<std::mutex> lock(helper_state->mutex);
const bool credential_ui_visible = IsCredentialUiVisible( const bool credential_ui_visible = IsCredentialUiVisible(
helper_state->session_locked, helper_state->logon_ui_visible, helper_state->session_locked, helper_state->logon_ui_visible,
helper_state->consent_ui_visible,
helper_state->input_desktop_available, helper_state->input_desktop_available,
helper_state->secure_desktop_active); helper_state->secure_desktop_active);
const bool unlock_ui_visible = const bool unlock_ui_visible =
@@ -414,6 +430,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
json["input_desktop"] = helper_state->input_desktop_name; json["input_desktop"] = helper_state->input_desktop_name;
json["lock_app_visible"] = helper_state->lock_app_visible; json["lock_app_visible"] = helper_state->lock_app_visible;
json["logon_ui_visible"] = helper_state->logon_ui_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["secure_desktop_active"] = helper_state->secure_desktop_active;
json["credential_ui_visible"] = credential_ui_visible; json["credential_ui_visible"] = credential_ui_visible;
json["unlock_ui_visible"] = unlock_ui_visible; json["unlock_ui_visible"] = unlock_ui_visible;
@@ -639,8 +656,12 @@ bool EnsureThreadInteractiveDesktop(HDESK* opened_desktop_out = nullptr) {
return false; return false;
} }
const wchar_t* DesktopNameForInteractiveStage( std::wstring DesktopNameForInteractiveStage(
const std::string& interactive_stage) { const std::string& interactive_stage,
const std::string& interactive_desktop) {
if (!interactive_desktop.empty()) {
return Utf8ToWide(interactive_desktop);
}
if (interactive_stage == "credential-ui" || if (interactive_stage == "credential-ui" ||
interactive_stage == "secure-desktop") { interactive_stage == "secure-desktop") {
return L"Winlogon"; return L"Winlogon";
@@ -648,7 +669,7 @@ const wchar_t* DesktopNameForInteractiveStage(
if (interactive_stage == "lock-screen") { if (interactive_stage == "lock-screen") {
return L"Default"; return L"Default";
} }
return nullptr; return {};
} }
struct DesktopSwitchDetails { struct DesktopSwitchDetails {
@@ -666,14 +687,14 @@ struct InputInjectionResult {
}; };
DesktopSwitchDetails BuildDesktopSwitchDetails( DesktopSwitchDetails BuildDesktopSwitchDetails(
const std::string& interactive_stage) { const std::string& interactive_stage,
const std::string& interactive_desktop) {
DesktopSwitchDetails details; DesktopSwitchDetails details;
details.stage = interactive_stage; details.stage = interactive_stage;
const wchar_t* desktop_name = const std::wstring desktop_name =
DesktopNameForInteractiveStage(interactive_stage); DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
details.target_desktop = details.target_desktop =
desktop_name != nullptr ? WideToUtf8(std::wstring(desktop_name)) !desktop_name.empty() ? WideToUtf8(desktop_name) : "input-or-Winlogon";
: "input-or-Winlogon";
details.current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW()); details.current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW());
return details; return details;
} }
@@ -709,6 +730,7 @@ Json BuildInputFailureJson(const InputInjectionResult& result) {
bool EnsureThreadInteractiveDesktopForStage( bool EnsureThreadInteractiveDesktopForStage(
const std::string& interactive_stage, const std::string& interactive_stage,
const std::string& interactive_desktop,
HDESK* opened_desktop_out = nullptr, HDESK* opened_desktop_out = nullptr,
DesktopSwitchDetails* switch_details = nullptr) { DesktopSwitchDetails* switch_details = nullptr) {
if (opened_desktop_out != nullptr) { if (opened_desktop_out != nullptr) {
@@ -716,15 +738,15 @@ bool EnsureThreadInteractiveDesktopForStage(
} }
DesktopSwitchDetails local_details = DesktopSwitchDetails local_details =
BuildDesktopSwitchDetails(interactive_stage); BuildDesktopSwitchDetails(interactive_stage, interactive_desktop);
if (switch_details != nullptr) { if (switch_details != nullptr) {
*switch_details = local_details; *switch_details = local_details;
} }
const wchar_t* desktop_name = const std::wstring desktop_name =
DesktopNameForInteractiveStage(interactive_stage); DesktopNameForInteractiveStage(interactive_stage, interactive_desktop);
if (desktop_name != nullptr) { if (!desktop_name.empty()) {
if (EnsureThreadDesktop(desktop_name, opened_desktop_out)) { if (EnsureThreadDesktop(desktop_name.c_str(), opened_desktop_out)) {
if (switch_details != nullptr) { if (switch_details != nullptr) {
switch_details->current_desktop = switch_details->current_desktop =
WideToUtf8(GetCurrentThreadDesktopNameW()); WideToUtf8(GetCurrentThreadDesktopNameW());
@@ -741,7 +763,7 @@ bool EnsureThreadInteractiveDesktopForStage(
LOG_WARN( LOG_WARN(
"Failed to switch secure input helper to stage desktop, stage='{}', " "Failed to switch secure input helper to stage desktop, stage='{}', "
"desktop='{}', error={}, current='{}'", "desktop='{}', error={}, current='{}'",
interactive_stage, WideToUtf8(std::wstring(desktop_name)), interactive_stage, WideToUtf8(desktop_name),
error, error,
WideToUtf8(GetCurrentThreadDesktopNameW())); WideToUtf8(GetCurrentThreadDesktopNameW()));
SetLastError(error); SetLastError(error);
@@ -790,10 +812,12 @@ bool PreferSideSpecificVkInjection(int key_code) {
InputInjectionResult InjectKeyboardInput( InputInjectionResult InjectKeyboardInput(
int key_code, bool is_down, uint32_t scan_code, bool extended, 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; ScopedDesktopHandle desktop;
DesktopSwitchDetails desktop_switch; DesktopSwitchDetails desktop_switch;
if (!EnsureThreadInteractiveDesktopForStage(interactive_stage, if (!EnsureThreadInteractiveDesktopForStage(interactive_stage,
interactive_desktop,
&desktop.handle, &desktop.handle,
&desktop_switch)) { &desktop_switch)) {
const DWORD error = GetLastError(); const DWORD error = GetLastError();
@@ -851,11 +875,37 @@ InputInjectionResult InjectKeyboardInput(
return BuildInputSuccess(); 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, bool ParseSecureInputKeyboardCommand(const std::string& command,
int* key_code_out, bool* is_down_out, int* key_code_out, bool* is_down_out,
uint32_t* scan_code_out, uint32_t* scan_code_out,
bool* extended_out, bool* extended_out,
std::string* interactive_stage_out = std::string* interactive_stage_out =
nullptr,
std::string* interactive_desktop_out =
nullptr) { nullptr) {
if (key_code_out == nullptr || is_down_out == nullptr || if (key_code_out == nullptr || is_down_out == nullptr ||
scan_code_out == nullptr || extended_out == nullptr) { scan_code_out == nullptr || extended_out == nullptr) {
@@ -867,6 +917,9 @@ bool ParseSecureInputKeyboardCommand(const std::string& command,
if (interactive_stage_out != nullptr) { if (interactive_stage_out != nullptr) {
interactive_stage_out->clear(); interactive_stage_out->clear();
} }
if (interactive_desktop_out != nullptr) {
interactive_desktop_out->clear();
}
if (command.rfind(crossdesk::kCrossDeskSecureInputKeyboardCommandPrefix, 0) != if (command.rfind(crossdesk::kCrossDeskSecureInputKeyboardCommandPrefix, 0) !=
0) { 0) {
@@ -926,8 +979,9 @@ bool ParseSecureInputKeyboardCommand(const std::string& command,
: command.substr(extended_separator + 1, : command.substr(extended_separator + 1,
stage_separator - extended_separator - 1); stage_separator - extended_separator - 1);
if (stage_separator != std::string::npos && if (stage_separator != std::string::npos &&
interactive_stage_out != nullptr) { (interactive_stage_out != nullptr || interactive_desktop_out != nullptr)) {
*interactive_stage_out = command.substr(stage_separator + 1); ParseInteractionTail(command.substr(stage_separator + 1),
interactive_stage_out, interactive_desktop_out);
} }
if (extended_str == "1" || extended_str == "true") { if (extended_str == "1" || extended_str == "true") {
*extended_out = true; *extended_out = true;
@@ -951,6 +1005,7 @@ bool ParseSecureInputMouseCommand(const std::string& command,
return false; return false;
} }
request_out->interactive_stage.clear(); request_out->interactive_stage.clear();
request_out->interactive_desktop.clear();
const size_t x_begin = const size_t x_begin =
std::strlen(crossdesk::kCrossDeskSecureInputMouseCommandPrefix); std::strlen(crossdesk::kCrossDeskSecureInputMouseCommandPrefix);
@@ -993,7 +1048,9 @@ bool ParseSecureInputMouseCommand(const std::string& command,
? command.substr(flag_begin) ? command.substr(flag_begin)
: command.substr(flag_begin, stage_separator - flag_begin)); : command.substr(flag_begin, stage_separator - flag_begin));
if (stage_separator != std::string::npos) { 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 (...) { } catch (...) {
return false; return false;
@@ -1013,6 +1070,7 @@ bool ParseSecureInputCaptureCommand(const std::string& command,
return false; return false;
} }
request_out->interactive_stage.clear(); request_out->interactive_stage.clear();
request_out->interactive_desktop.clear();
const size_t values_begin = const size_t values_begin =
std::strlen(crossdesk::kCrossDeskSecureInputCaptureCommandPrefix); std::strlen(crossdesk::kCrossDeskSecureInputCaptureCommandPrefix);
@@ -1042,9 +1100,11 @@ bool ParseSecureInputCaptureCommand(const std::string& command,
request_out->width = parsed_values[2] & ~1; request_out->width = parsed_values[2] & ~1;
request_out->height = parsed_values[3] & ~1; request_out->height = parsed_values[3] & ~1;
request_out->show_cursor = parsed_values[4] != 0; request_out->show_cursor = parsed_values[4] != 0;
request_out->interactive_stage = if (separator != std::string::npos) {
separator == std::string::npos ? std::string() ParseInteractionTail(command.substr(token_begin),
: command.substr(token_begin); &request_out->interactive_stage,
&request_out->interactive_desktop);
}
return request_out->width > 0 && request_out->height > 0; return request_out->width > 0 && request_out->height > 0;
} }
@@ -1059,6 +1119,7 @@ bool ParseSecureInputCaptureStartCommand(const std::string& command,
return false; return false;
} }
request_out->interactive_stage.clear(); request_out->interactive_stage.clear();
request_out->interactive_desktop.clear();
const size_t values_begin = std::strlen( const size_t values_begin = std::strlen(
crossdesk::kCrossDeskSecureInputCaptureStartCommandPrefix); crossdesk::kCrossDeskSecureInputCaptureStartCommandPrefix);
@@ -1089,9 +1150,11 @@ bool ParseSecureInputCaptureStartCommand(const std::string& command,
request_out->height = parsed_values[3] & ~1; request_out->height = parsed_values[3] & ~1;
request_out->show_cursor = parsed_values[4] != 0; request_out->show_cursor = parsed_values[4] != 0;
request_out->fps = parsed_values[5] > 0 ? parsed_values[5] : 30; request_out->fps = parsed_values[5] > 0 ? parsed_values[5] : 30;
request_out->interactive_stage = if (separator != std::string::npos) {
separator == std::string::npos ? std::string() ParseInteractionTail(command.substr(token_begin),
: command.substr(token_begin); &request_out->interactive_stage,
&request_out->interactive_desktop);
}
return request_out->width > 0 && request_out->height > 0; return request_out->width > 0 && request_out->height > 0;
} }
@@ -1129,6 +1192,7 @@ InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) {
ScopedDesktopHandle desktop; ScopedDesktopHandle desktop;
DesktopSwitchDetails desktop_switch; DesktopSwitchDetails desktop_switch;
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage, if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
request.interactive_desktop,
&desktop.handle, &desktop.handle,
&desktop_switch)) { &desktop_switch)) {
const DWORD error = GetLastError(); const DWORD error = GetLastError();
@@ -1205,6 +1269,7 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
ScopedDesktopHandle desktop; ScopedDesktopHandle desktop;
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage, if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
request.interactive_desktop,
&desktop.handle)) { &desktop.handle)) {
const DWORD error = GetLastError(); const DWORD error = GetLastError();
return BuildTextResponseBytes(BuildErrorJson( return BuildTextResponseBytes(BuildErrorJson(
@@ -1355,6 +1420,7 @@ void SecureDesktopSharedCaptureThread(
ScopedDesktopHandle desktop; ScopedDesktopHandle desktop;
if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage, if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage,
request.interactive_desktop,
&desktop.handle)) { &desktop.handle)) {
LOG_ERROR("Secure shared capture desktop switch failed, error={}", LOG_ERROR("Secure shared capture desktop switch failed, error={}",
GetLastError()); GetLastError());
@@ -1607,11 +1673,13 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
uint32_t scan_code = 0; uint32_t scan_code = 0;
bool extended = false; bool extended = false;
std::string interactive_stage; std::string interactive_stage;
std::string interactive_desktop;
if (ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code, if (ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,
&extended, &interactive_stage)) { &extended, &interactive_stage,
&interactive_desktop)) {
const InputInjectionResult inject_result = const InputInjectionResult inject_result =
InjectKeyboardInput(key_code, is_down, scan_code, extended, InjectKeyboardInput(key_code, is_down, scan_code, extended,
interactive_stage); interactive_stage, interactive_desktop);
if (!inject_result.ok) { if (!inject_result.ok) {
LOG_WARN( LOG_WARN(
"Secure input helper input failed for key_code={}, is_down={}, " "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["scan_code"] = scan_code;
json["extended"] = extended; json["extended"] = extended;
json["stage"] = interactive_stage; json["stage"] = interactive_stage;
json["interactive_desktop"] = interactive_desktop;
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW()); json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
return BuildTextResponseBytes(json.dump()); return BuildTextResponseBytes(json.dump());
} }
@@ -1658,6 +1727,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
json["wheel"] = mouse_request.wheel; json["wheel"] = mouse_request.wheel;
json["flag"] = mouse_request.flag; json["flag"] = mouse_request.flag;
json["stage"] = mouse_request.interactive_stage; json["stage"] = mouse_request.interactive_stage;
json["interactive_desktop"] = mouse_request.interactive_desktop;
json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW()); json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW());
return BuildTextResponseBytes(json.dump()); return BuildTextResponseBytes(json.dump());
} }
@@ -1887,6 +1957,7 @@ int main(int argc, char* argv[]) {
std::string last_desktop_name; std::string last_desktop_name;
bool last_lock_app = false; bool last_lock_app = false;
bool last_logon_ui = false; bool last_logon_ui = false;
bool last_consent_ui = false;
bool last_secure_desktop = false; bool last_secure_desktop = false;
bool last_session_locked = false; bool last_session_locked = false;
std::string last_stage; std::string last_stage;
@@ -1898,6 +1969,7 @@ int main(int argc, char* argv[]) {
bool input_desktop_available = false; bool input_desktop_available = false;
bool lock_app_visible = false; bool lock_app_visible = false;
bool logon_ui_running = false; bool logon_ui_running = false;
bool consent_ui_visible = false;
bool secure_desktop_active = false; bool secure_desktop_active = false;
{ {
std::lock_guard<std::mutex> lock(helper_state.mutex); 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; input_desktop_available = helper_state.input_desktop_available;
lock_app_visible = helper_state.lock_app_visible; lock_app_visible = helper_state.lock_app_visible;
logon_ui_running = helper_state.logon_ui_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; secure_desktop_active = helper_state.secure_desktop_active;
} }
const bool credential_ui_visible = const bool credential_ui_visible =
IsCredentialUiVisible(session_locked, logon_ui_running, IsCredentialUiVisible(session_locked, logon_ui_running,
consent_ui_visible,
input_desktop_available, input_desktop_available,
secure_desktop_active); secure_desktop_active);
std::string stage = DetermineInteractiveStage( std::string stage = DetermineInteractiveStage(
@@ -1919,17 +1993,19 @@ int main(int argc, char* argv[]) {
session_locked != last_session_locked || session_locked != last_session_locked ||
lock_app_visible != last_lock_app || lock_app_visible != last_lock_app ||
logon_ui_running != last_logon_ui || logon_ui_running != last_logon_ui ||
consent_ui_visible != last_consent_ui ||
secure_desktop_active != last_secure_desktop || stage != last_stage) { secure_desktop_active != last_secure_desktop || stage != last_stage) {
LOG_INFO( LOG_INFO(
"Session helper state: session_id={}, input_desktop='{}', " "Session helper state: session_id={}, input_desktop='{}', "
"session_locked={}, lock_app_visible={}, logon_ui_running={}, " "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, 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_desktop_name = desktop_name;
last_session_locked = session_locked; last_session_locked = session_locked;
last_lock_app = lock_app_visible; last_lock_app = lock_app_visible;
last_logon_ui = logon_ui_running; last_logon_ui = logon_ui_running;
last_consent_ui = consent_ui_visible;
last_secure_desktop = secure_desktop_active; last_secure_desktop = secure_desktop_active;
last_stage = stage; last_stage = stage;
} }
@@ -9,6 +9,17 @@ namespace crossdesk {
class SpeakerCapturerMacosx; class SpeakerCapturerMacosx;
} }
namespace {
std::string NSErrorToString(NSError* error) {
if (!error) {
return "";
}
const char* description = [error.localizedDescription UTF8String];
return description ? description : "";
}
} // namespace
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput> @interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner; @property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner; - (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
@@ -28,15 +39,36 @@ class SpeakerCapturerMacosx;
ofType:(SCStreamOutputType)type { ofType:(SCStreamOutputType)type {
if (type != SCStreamOutputTypeAudio) return; if (type != SCStreamOutputTypeAudio) return;
crossdesk::SpeakerCapturerMacosx* owner = _owner;
if (!owner || !owner->cb_) {
return;
}
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
if (!blockBuffer) {
return;
}
size_t length = CMBlockBufferGetDataLength(blockBuffer); size_t length = CMBlockBufferGetDataLength(blockBuffer);
char* dataPtr = NULL; char* dataPtr = NULL;
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr); OSStatus dataStatus =
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
if (dataStatus != noErr || dataPtr == nullptr || length == 0) {
return;
}
CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
if (!formatDesc) {
return;
}
const AudioStreamBasicDescription* asbd = const AudioStreamBasicDescription* asbd =
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc); CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
if (!asbd || asbd->mChannelsPerFrame == 0) {
return;
}
if (_owner->cb_ && dataPtr && length > 0 && asbd) { if (owner->cb_) {
std::vector<short> out_pcm16; std::vector<short> out_pcm16;
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) { if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
int channels = asbd->mChannelsPerFrame; int channels = asbd->mChannelsPerFrame;
@@ -86,7 +118,10 @@ class SpeakerCapturerMacosx;
size_t total_bytes = out_pcm16.size() * sizeof(short); size_t total_bytes = out_pcm16.size() * sizeof(short);
unsigned char* p = (unsigned char*)out_pcm16.data(); unsigned char* p = (unsigned char*)out_pcm16.data();
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) { for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) {
_owner->cb_(p + offset, frame_bytes, "audio"); if (!owner->cb_) {
return;
}
owner->cb_(p + offset, frame_bytes, "audio");
} }
} }
} }
@@ -155,7 +190,7 @@ int SpeakerCapturerMacosx::Init(speaker_data_cb cb) {
if (error || !impl_->content) { if (error || !impl_->content) {
LOG_ERROR("Failed to get shareable content: {}", LOG_ERROR("Failed to get shareable content: {}",
std::string([error.localizedDescription UTF8String])); NSErrorToString(error));
return -1; return -1;
} }
@@ -209,7 +244,7 @@ int SpeakerCapturerMacosx::Start() {
error:&addOutputError]; error:&addOutputError];
if (!ok || addOutputError) { if (!ok || addOutputError) {
LOG_ERROR("addStreamOutput error: {}", LOG_ERROR("addStreamOutput error: {}",
std::string([addOutputError.localizedDescription UTF8String])); NSErrorToString(addOutputError));
impl_->stream = nil; impl_->stream = nil;
impl_->delegate = nil; impl_->delegate = nil;
return -1; return -1;
@@ -220,7 +255,7 @@ int SpeakerCapturerMacosx::Start() {
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) { [impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
if (error) { if (error) {
LOG_ERROR("startCaptureWithCompletionHandler error: {}", LOG_ERROR("startCaptureWithCompletionHandler error: {}",
std::string([error.localizedDescription UTF8String])); NSErrorToString(error));
ret = -1; ret = -1;
} }
dispatch_semaphore_signal(semaStart); dispatch_semaphore_signal(semaStart);
@@ -238,13 +273,14 @@ int SpeakerCapturerMacosx::Stop() {
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) { [impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
if (error) { if (error) {
LOG_ERROR("stopCaptureWithCompletionHandler error: {}", LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
std::string([error.localizedDescription UTF8String])); NSErrorToString(error));
} }
dispatch_semaphore_signal(sema); dispatch_semaphore_signal(sema);
}]; }];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
impl_->stream = nil; impl_->stream = nil;
impl_->delegate.owner = nullptr;
impl_->delegate = nil; impl_->delegate = nil;
return 0; return 0;
+16 -9
View File
@@ -225,9 +225,12 @@ int Thumbnail::LoadThumbnail(
std::string remote_id; std::string remote_id;
std::string cipher_password; std::string cipher_password;
std::string remote_host_name; std::string remote_host_name;
std::string password;
std::string original_image_name; std::string original_image_name;
bool remember_password = false;
if ('Y' == cipher_image_name[9] && cipher_image_name.size() >= 16) { if (cipher_image_name.size() > 9 && 'Y' == cipher_image_name[9] &&
cipher_image_name.size() >= 16) {
size_t pos_y = cipher_image_name.find('Y'); size_t pos_y = cipher_image_name.find('Y');
size_t pos_at = cipher_image_name.find('@'); size_t pos_at = cipher_image_name.find('@');
@@ -241,10 +244,11 @@ int Thumbnail::LoadThumbnail(
remote_host_name = remote_host_name =
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1); cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
cipher_password = cipher_image_name.substr(pos_at + 1); cipher_password = cipher_image_name.substr(pos_at + 1);
password = AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
remember_password = true;
original_image_name = original_image_name = remote_id + 'Y' + remote_host_name + "@" +
remote_id + 'Y' + remote_host_name + "@" + password;
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
} else { } else {
size_t pos_n = cipher_image_name.find('N'); size_t pos_n = cipher_image_name.find('N');
// size_t pos_at = cipher_image_name.find('@'); // size_t pos_at = cipher_image_name.find('@');
@@ -257,16 +261,19 @@ int Thumbnail::LoadThumbnail(
remote_id = cipher_image_name.substr(0, pos_n); remote_id = cipher_image_name.substr(0, pos_n);
remote_host_name = cipher_image_name.substr(pos_n + 1); remote_host_name = cipher_image_name.substr(pos_n + 1);
original_image_name = original_image_name = remote_id + 'N' + remote_host_name;
remote_id + 'N' + remote_host_name + "@" +
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
} }
std::string image_path = save_path_ + cipher_image_name; std::string image_path = save_path_ + cipher_image_name;
Thumbnail::RecentConnection recent_connection;
recent_connection.remote_id = remote_id;
recent_connection.remote_host_name = remote_host_name;
recent_connection.password = password;
recent_connection.remember_password = remember_password;
recent_connections.emplace_back( recent_connections.emplace_back(
std::make_pair(original_image_name, Thumbnail::RecentConnection())); std::make_pair(original_image_name, recent_connection));
LoadTextureFromFile(image_path.c_str(), renderer, LoadTextureFromFile(image_path.c_str(), renderer,
&(recent_connections[i].second.texture), width, &(recent_connections.back().second.texture), width,
height); height);
} }
return 0; return 0;
+350 -69
View File
@@ -8,7 +8,15 @@
#include <httplib.h> #include <httplib.h>
#include "rd_log.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <iostream> #include <iostream>
#include <limits>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -16,12 +24,296 @@
namespace crossdesk { namespace crossdesk {
static std::string latest_release_date_ = ""; static std::string latest_release_date_ = "";
static bool latest_patch_available_ = false;
static int latest_patch_ = 0;
std::vector<int> SplitVersion(const std::string& ver);
namespace {
constexpr size_t kMaxInlinePatchDigits = 4;
struct ParsedVersion {
std::vector<int> numbers;
std::string date;
bool has_patch = false;
int patch = 0;
};
bool IsDigit(char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
}
bool IsAlphaNumeric(char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0;
}
bool IsAllDigits(const std::string& value) {
if (value.empty()) {
return false;
}
for (char c : value) {
if (!IsDigit(c)) {
return false;
}
}
return true;
}
bool TryParseNonNegativeInt(const std::string& value, int* result) {
if (!IsAllDigits(value)) {
return false;
}
try {
const long long parsed = std::stoll(value);
if (parsed > std::numeric_limits<int>::max()) {
return false;
}
*result = static_cast<int>(parsed);
return true;
} catch (...) {
return false;
}
}
bool TryParseInlinePatch(const std::string& value, int* result) {
if (value.size() > kMaxInlinePatchDigits) {
return false;
}
return TryParseNonNegativeInt(value, result);
}
size_t FindNumericStart(const std::string& version) {
size_t start = 0;
while (start < version.size() && !IsDigit(version[start])) {
start++;
}
return start;
}
size_t FindNumericEnd(const std::string& version, size_t start) {
size_t end = start;
while (end < version.size() &&
(IsDigit(version[end]) || version[end] == '.')) {
end++;
}
return end;
}
bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) {
const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]);
const size_t end = pos + len;
const bool after_ok = end >= value.size() || !IsDigit(value[end]);
return before_ok && after_ok;
}
bool IsCompactDateAt(const std::string& value, size_t pos) {
if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) {
return false;
}
for (size_t i = 0; i < 8; ++i) {
if (!IsDigit(value[pos + i])) {
return false;
}
}
return true;
}
std::string CompactDateToIso(const std::string& compact_date) {
return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" +
compact_date.substr(6, 2);
}
bool ExtractDateFromText(const std::string& value,
std::string* date,
size_t* date_end) {
for (size_t i = 0; i < value.size(); ++i) {
if (IsCompactDateAt(value, i)) {
*date = CompactDateToIso(value.substr(i, 8));
*date_end = i + 8;
return true;
}
}
return false;
}
ParsedVersion ParseVersion(const std::string& version) {
const size_t numeric_start = FindNumericStart(version);
const size_t numeric_end = FindNumericEnd(version, numeric_start);
ParsedVersion parsed;
parsed.numbers = SplitVersion(version.substr(numeric_start,
numeric_end - numeric_start));
const std::string suffix = version.substr(numeric_end);
size_t pos = 0;
while (pos < suffix.size()) {
while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) {
pos++;
}
const size_t token_start = pos;
while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) {
pos++;
}
if (token_start == pos) {
continue;
}
const std::string token = suffix.substr(token_start, pos - token_start);
if (parsed.date.empty() && IsCompactDateAt(token, 0)) {
parsed.date = CompactDateToIso(token);
continue;
}
int patch = 0;
if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) {
parsed.has_patch = true;
parsed.patch = patch;
}
}
return parsed;
}
int CompareNumericVersion(const std::vector<int>& current,
const std::vector<int>& latest) {
std::vector<int> current_parts = current;
std::vector<int> latest_parts = latest;
const size_t len = std::max(current_parts.size(), latest_parts.size());
current_parts.resize(len, 0);
latest_parts.resize(len, 0);
for (size_t i = 0; i < len; ++i) {
if (latest_parts[i] > current_parts[i]) {
return 1;
}
if (latest_parts[i] < current_parts[i]) {
return -1;
}
}
return 0;
}
void ResetLatestMetadata() {
latest_release_date_ = "";
latest_patch_available_ = false;
latest_patch_ = 0;
}
bool ReadPatchField(const nlohmann::json& json, int* patch) {
if (!json.contains("patch")) {
return false;
}
const auto& patch_value = json["patch"];
if (patch_value.is_number_integer()) {
const long long parsed = patch_value.get<long long>();
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
return false;
}
*patch = static_cast<int>(parsed);
return true;
}
if (patch_value.is_string()) {
return TryParseNonNegativeInt(patch_value.get<std::string>(), patch);
}
return false;
}
void LogHttpError(const httplib::Result& result) {
LOG_WARN("Failed to fetch version.json: error={}, message={}",
static_cast<int>(result.error()), httplib::to_string(result.error()));
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(),
result.ssl_openssl_error());
#endif
}
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
bool PathExists(const std::string& path) {
if (path.empty()) {
return false;
}
std::error_code ec;
return std::filesystem::exists(path, ec);
}
std::string GetEnvPathIfExists(const char* key) {
const char* value = std::getenv(key);
if (!value) {
return "";
}
const std::string path = value;
return PathExists(path) ? path : "";
}
std::string FindFirstExistingPath(
const std::vector<std::string>& candidates) {
for (const auto& candidate : candidates) {
if (PathExists(candidate)) {
return candidate;
}
}
return "";
}
void ConfigureLinuxCaCerts(httplib::Client* cli) {
const std::string ca_file = [&]() {
const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE");
if (!env_path.empty()) {
return env_path;
}
return FindFirstExistingPath({
"/etc/ssl/certs/ca-certificates.crt",
"/etc/pki/tls/certs/ca-bundle.crt",
"/etc/ssl/cert.pem",
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
});
}();
const std::string ca_dir = [&]() {
const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR");
if (!env_path.empty()) {
return env_path;
}
return FindFirstExistingPath({
"/etc/ssl/certs",
"/etc/pki/tls/certs",
"/etc/openssl/certs",
});
}();
if (ca_file.empty() && ca_dir.empty()) {
LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults");
return;
}
cli->set_ca_cert_path(ca_file, ca_dir);
LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}",
ca_file.empty() ? "<none>" : ca_file,
ca_dir.empty() ? "<none>" : ca_dir);
}
#endif
} // namespace
std::string ExtractNumericPart(const std::string& ver) { std::string ExtractNumericPart(const std::string& ver) {
size_t start = 0; const size_t start = FindNumericStart(ver);
while (start < ver.size() && !std::isdigit(ver[start])) start++; const size_t end = FindNumericEnd(ver, start);
size_t end = start;
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
return ver.substr(start, end - start); return ver.substr(start, end - start);
} }
@@ -42,25 +334,13 @@ std::vector<int> SplitVersion(const std::string& ver) {
// extract date from version string (format: v1.2.3-20251113-abc // extract date from version string (format: v1.2.3-20251113-abc
// or 1.2.3-20251113-abc) // or 1.2.3-20251113-abc)
std::string ExtractDateFromVersion(const std::string& version) { std::string ExtractDateFromVersion(const std::string& version) {
size_t dash1 = version.find('-'); const size_t numeric_start = FindNumericStart(version);
if (dash1 != std::string::npos) { const size_t numeric_end = FindNumericEnd(version, numeric_start);
size_t dash2 = version.find('-', dash1 + 1); const std::string suffix = version.substr(numeric_end);
if (dash2 != std::string::npos) { std::string date;
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1); size_t date_end = 0;
if (ExtractDateFromText(suffix, &date, &date_end)) {
bool is_date = true; return date;
for (char c : date_part) {
if (!std::isdigit(c)) {
is_date = false;
break;
}
}
if (is_date) {
// convert YYYYMMDD to YYYY-MM-DD
return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" +
date_part.substr(6, 2);
}
}
} }
return ""; return "";
} }
@@ -73,55 +353,41 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
} }
bool IsNewerVersion(const std::string& current, const std::string& latest) { bool IsNewerVersion(const std::string& current, const std::string& latest) {
auto v1 = SplitVersion(ExtractNumericPart(current)); return IsNewerVersionWithMetadata(
auto v2 = SplitVersion(ExtractNumericPart(latest)); current, latest, latest_release_date_,
latest_patch_available_ ? latest_patch_ : -1);
size_t len = std::max(v1.size(), v2.size());
v1.resize(len, 0);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) {
if (v2[i] > v1[i]) return true;
if (v2[i] < v1[i]) return false;
}
// if versions are equal, compare by release date
if (!latest_release_date_.empty()) {
// try to extract date from current version string
std::string current_date = ExtractDateFromVersion(current);
if (!current_date.empty()) {
return IsNewerDate(current_date, latest_release_date_);
} else {
return true;
}
}
return false;
} }
bool IsNewerVersionWithDate(const std::string& current_version, bool IsNewerVersionWithMetadata(const std::string& current,
const std::string& current_date, const std::string& latest,
const std::string& latest_version, const std::string& latest_date,
const std::string& latest_date) { int latest_patch) {
// compare versions (void)latest_date;
auto v1 = SplitVersion(ExtractNumericPart(current_version));
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
size_t len = std::max(v1.size(), v2.size()); const ParsedVersion current_version = ParseVersion(current);
v1.resize(len, 0); const ParsedVersion latest_version = ParseVersion(latest);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) { const int numeric_compare =
if (v2[i] > v1[i]) return true; CompareNumericVersion(current_version.numbers, latest_version.numbers);
if (v2[i] < v1[i]) return false; if (numeric_compare > 0) {
return true;
}
if (numeric_compare < 0) {
return false;
} }
// if versions are equal, compare by release date const bool metadata_has_patch = latest_patch >= 0;
if (!current_date.empty() && !latest_date.empty()) { const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
return IsNewerDate(current_date, latest_date); if (latest_has_patch || current_version.has_patch) {
const int resolved_latest_patch =
metadata_has_patch ? latest_patch
: (latest_version.has_patch ? latest_version.patch
: 0);
const int resolved_current_patch =
current_version.has_patch ? current_version.patch : 0;
return resolved_latest_patch > resolved_current_patch;
} }
// if dates are not available, versions are equal
return false; return false;
} }
@@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() {
cli.set_connection_timeout(5); cli.set_connection_timeout(5);
cli.set_read_timeout(5); cli.set_read_timeout(5);
cli.set_follow_location(true);
if (auto res = cli.Get("/version.json")) { #if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
ConfigureLinuxCaCerts(&cli);
#endif
auto res = cli.Get("/version.json");
if (res) {
if (res->status == 200) { if (res->status == 200) {
try { try {
auto j = nlohmann::json::parse(res->body); auto j = nlohmann::json::parse(res->body);
@@ -140,17 +412,26 @@ nlohmann::json CheckUpdate() {
} else { } else {
latest_release_date_ = ""; latest_release_date_ = "";
} }
latest_patch_ = 0;
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
j.value("latest_version", j.value("version", "")),
j.value("releaseDate", ""),
latest_patch_available_ ? latest_patch_ : -1);
return j; return j;
} catch (std::exception&) { } catch (const std::exception& e) {
latest_release_date_ = ""; LOG_WARN("Failed to parse version.json: {}", e.what());
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} else { } else {
latest_release_date_ = ""; LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} else { } else {
latest_release_date_ = ""; LogHttpError(res);
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} }
+6
View File
@@ -16,6 +16,12 @@ nlohmann::json CheckUpdate();
bool IsNewerVersion(const std::string& current, const std::string& latest); bool IsNewerVersion(const std::string& current, const std::string& latest);
// Pass latest_patch < 0 when patch metadata is unavailable.
bool IsNewerVersionWithMetadata(const std::string& current,
const std::string& latest,
const std::string& latest_date,
int latest_patch);
} // namespace crossdesk } // namespace crossdesk
#endif #endif

Some files were not shown because too many files have changed in this diff Show More