[refactor] reorganize platform and wire sources
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 746 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -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 Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="requireAdministrator" 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>
|
||||
@@ -0,0 +1,16 @@
|
||||
// Application icon resource; load by the resource name IDI_ICON1.
|
||||
IDI_ICON1 ICON "crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -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 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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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 "crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Portable"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -0,0 +1,4 @@
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Service"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_service.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -0,0 +1,18 @@
|
||||
// Session helper icon and manifest resources.
|
||||
IDI_ICON1 ICON "crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_PORTABLE
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
#elif defined(CROSSDESK_DEBUG)
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <winver.h>
|
||||
|
||||
#ifndef CROSSDESK_VERSION_STRING
|
||||
#define CROSSDESK_VERSION_STRING "0.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_VERSION_NUMERIC
|
||||
#define CROSSDESK_VERSION_NUMERIC 0,0,0,0
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_FILE_DESCRIPTION
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_ORIGINAL_FILENAME
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_FILE_TYPE
|
||||
#define CROSSDESK_FILE_TYPE VFT_APP
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION CROSSDESK_VERSION_NUMERIC
|
||||
PRODUCTVERSION CROSSDESK_VERSION_NUMERIC
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE CROSSDESK_FILE_TYPE
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "CrossDesk\0"
|
||||
VALUE "FileDescription", CROSSDESK_FILE_DESCRIPTION "\0"
|
||||
VALUE "FileVersion", CROSSDESK_VERSION_STRING "\0"
|
||||
VALUE "InternalName", CROSSDESK_ORIGINAL_FILENAME "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) CrossDesk contributors\0"
|
||||
VALUE "OriginalFilename", CROSSDESK_ORIGINAL_FILENAME "\0"
|
||||
VALUE "ProductName", "CrossDesk\0"
|
||||
VALUE "ProductVersion", CROSSDESK_VERSION_STRING "\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 1200
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,5 @@
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Windows Graphics Capture Plugin"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "wgc_plugin.dll"
|
||||
#define CROSSDESK_FILE_TYPE VFT_DLL
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 3 || $# -gt 4 ]]; then
|
||||
echo "Usage: $0 <debian-arch> <xmake-arch> <version> [extra-recommendation]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
DEBIAN_ARCH="$1"
|
||||
XMAKE_ARCH="$2"
|
||||
INPUT_VERSION="$3"
|
||||
EXTRA_RECOMMENDATION="${4:-}"
|
||||
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
TRAY_RUNTIME_RECOMMENDS="libayatana-appindicator3-1 | libappindicator3-1"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
find_slint_runtime() {
|
||||
local needed="$1"
|
||||
local resolved=""
|
||||
|
||||
resolved="$(ldd "$BUILD_BINARY" 2>/dev/null | awk -v needed="$needed" \
|
||||
'$1 == needed && $2 == "=>" && $3 ~ /^\// { print $3; exit }')"
|
||||
if [[ -n "$resolved" && -f "$resolved" ]]; then
|
||||
echo "$resolved"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local package_roots=()
|
||||
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
|
||||
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
|
||||
fi
|
||||
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
|
||||
|
||||
local package_root=""
|
||||
local candidate=""
|
||||
for package_root in "${package_roots[@]}"; do
|
||||
[[ -d "$package_root/s/slint" ]] || continue
|
||||
candidate="$(find "$package_root/s/slint" -path "*/lib/$needed" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$INPUT_VERSION")"
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
if [[ ! "$DEB_VERSION" =~ ^[0-9][0-9A-Za-z.+:~_-]*$ ]]; then
|
||||
echo "Invalid Debian version: $DEB_VERSION" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
BUILD_BINARY="$PROJECT_ROOT/build/linux/$XMAKE_ARCH/release/crossdesk"
|
||||
if [[ ! -x "$BUILD_BINARY" ]]; then
|
||||
echo "Missing release binary: $BUILD_BINARY" >&2
|
||||
echo "Build it first with: xmake f -m release -y && xmake b -vy crossdesk" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command_name in readelf ldd dpkg-deb; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Required packaging command is missing: $command_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-deb.XXXXXX")"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
DEB_DIR="$WORK_DIR/${PKG_NAME}-${DEB_VERSION}"
|
||||
DEBIAN_DIR="$DEB_DIR/DEBIAN"
|
||||
BIN_DIR="$DEB_DIR/usr/bin"
|
||||
PRIVATE_DIR="$DEB_DIR/usr/lib/$PKG_NAME"
|
||||
ICON_BASE_DIR="$DEB_DIR/usr/share/icons/hicolor"
|
||||
DESKTOP_DIR="$DEB_DIR/usr/share/applications"
|
||||
OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
|
||||
|
||||
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
|
||||
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
|
||||
|
||||
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
|
||||
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
|
||||
# runtime while using the host runtime on newer Ubuntu releases.
|
||||
if readelf -d "$BUILD_BINARY" \
|
||||
| grep -qE 'Shared library: \[libpipewire-0\.3\.so'; then
|
||||
echo "CrossDesk must not link directly to the optional PipeWire runtime" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Slint is intentionally built as a shared library. Keep that runtime private
|
||||
# to CrossDesk instead of relying on a non-existent distribution package.
|
||||
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
|
||||
's/.*Shared library: \[\(libslint_cpp[^]]*\)\].*/\1/p' | head -n 1)"
|
||||
if [[ -n "$SLINT_NEEDED" ]]; then
|
||||
SLINT_LIBRARY="$(find_slint_runtime "$SLINT_NEEDED" || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "Unable to locate the required Slint runtime: $SLINT_NEEDED" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
|
||||
fi
|
||||
|
||||
cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
|
||||
#!/bin/sh
|
||||
private_lib_dir="/usr/lib/crossdesk"
|
||||
if [ -n "${LD_LIBRARY_PATH:-}" ]; then
|
||||
export LD_LIBRARY_PATH="$private_lib_dir:$LD_LIBRARY_PATH"
|
||||
else
|
||||
export LD_LIBRARY_PATH="$private_lib_dir"
|
||||
fi
|
||||
exec /usr/lib/crossdesk/crossdesk-bin "$@"
|
||||
EOF
|
||||
chmod 0755 "$BIN_DIR/$PKG_NAME"
|
||||
ln -s "$PKG_NAME" "$BIN_DIR/$APP_NAME"
|
||||
|
||||
for size in 16 24 32 48 64 96 128 256; do
|
||||
install -d "$ICON_BASE_DIR/${size}x${size}/apps"
|
||||
install -m 0644 "$PROJECT_ROOT/apps/desktop/resources/linux/crossdesk_${size}x${size}.png" \
|
||||
"$ICON_BASE_DIR/${size}x${size}/apps/${PKG_NAME}.png"
|
||||
done
|
||||
|
||||
RECOMMENDS="$PORTAL_RUNTIME_RECOMMENDS, $TRAY_RUNTIME_RECOMMENDS"
|
||||
if [[ -n "$EXTRA_RECOMMENDATION" ]]; then
|
||||
RECOMMENDS="$RECOMMENDS, $EXTRA_RECOMMENDATION"
|
||||
fi
|
||||
|
||||
cat > "$DEBIAN_DIR/control" <<EOF
|
||||
Package: $PKG_NAME
|
||||
Version: $DEB_VERSION
|
||||
Architecture: $DEBIAN_ARCH
|
||||
Maintainer: $MAINTAINER
|
||||
Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
|
||||
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
|
||||
Recommends: $RECOMMENDS
|
||||
Priority: optional
|
||||
Section: utils
|
||||
EOF
|
||||
|
||||
cat > "$DESKTOP_DIR/$PKG_NAME.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Version=$DEB_VERSION
|
||||
Name=$APP_NAME
|
||||
Comment=$DESCRIPTION
|
||||
Exec=/usr/bin/$PKG_NAME
|
||||
Icon=$PKG_NAME
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
EOF
|
||||
|
||||
rm -f "$OUTPUT_FILE"
|
||||
dpkg-deb --root-owner-group --build "$DEB_DIR" "$OUTPUT_FILE"
|
||||
dpkg-deb --info "$OUTPUT_FILE" >/dev/null
|
||||
dpkg-deb --contents "$OUTPUT_FILE" > "$WORK_DIR/package-contents.txt"
|
||||
|
||||
if [[ -n "$SLINT_NEEDED" ]] && \
|
||||
! grep -q "usr/lib/$PKG_NAME/$SLINT_NEEDED" "$WORK_DIR/package-contents.txt"; then
|
||||
echo "Packaged artifact is missing $SLINT_NEEDED" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Deb package created: $OUTPUT_FILE"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec bash "$SCRIPT_DIR/package_deb.sh" amd64 x86_64 "$1" nvidia-cuda-toolkit
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 <version>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
exec bash "$SCRIPT_DIR/package_deb.sh" arm64 arm64 "$1"
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Usage: $0 <app-bundle> <expected-architecture>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
APP_BUNDLE="$1"
|
||||
EXPECTED_ARCH="$2"
|
||||
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
|
||||
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
|
||||
|
||||
for command_name in codesign install_name_tool lipo otool; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Required bundling command is missing: $command_name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -x "$APP_EXECUTABLE" ]]; then
|
||||
echo "Missing app executable: $APP_EXECUTABLE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "App executable does not contain the expected architecture: $EXPECTED_ARCH" >&2
|
||||
lipo -archs "$APP_EXECUTABLE" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLINT_REFERENCE="$(otool -L "$APP_EXECUTABLE" | awk \
|
||||
'$1 ~ /(^|\/)libslint_cpp[^\/[:space:]]*\.dylib$/ { print $1; exit }')"
|
||||
if [[ -z "$SLINT_REFERENCE" ]]; then
|
||||
echo "The app executable does not reference the Slint runtime" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SLINT_NAME="${SLINT_REFERENCE##*/}"
|
||||
SLINT_CURRENT_VERSION="$(otool -L "$APP_EXECUTABLE" | awk -v dependency="$SLINT_REFERENCE" '
|
||||
$1 == dependency {
|
||||
for (i = 1; i <= NF; ++i) {
|
||||
if ($i == "current") {
|
||||
version = $(i + 2)
|
||||
gsub(/[),]/, "", version)
|
||||
print version
|
||||
exit
|
||||
}
|
||||
}
|
||||
}')"
|
||||
|
||||
slint_runtime_version() {
|
||||
local library="$1"
|
||||
otool -L "$library" | awk -v name="$SLINT_NAME" '
|
||||
index($1, "/" name) || $1 == "@rpath/" name {
|
||||
for (i = 1; i <= NF; ++i) {
|
||||
if ($i == "current") {
|
||||
version = $(i + 2)
|
||||
gsub(/[),]/, "", version)
|
||||
print version
|
||||
exit
|
||||
}
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
find_slint_runtime() {
|
||||
local candidate=""
|
||||
local candidate_version=""
|
||||
local package_root=""
|
||||
local package_roots=()
|
||||
|
||||
if [[ "$SLINT_REFERENCE" == /* && -f "$SLINT_REFERENCE" ]]; then
|
||||
if lipo "$SLINT_REFERENCE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "$SLINT_REFERENCE"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
|
||||
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
|
||||
fi
|
||||
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
|
||||
|
||||
for package_root in "${package_roots[@]}"; do
|
||||
[[ -d "$package_root/s/slint" ]] || continue
|
||||
while IFS= read -r candidate; do
|
||||
if ! lipo "$candidate" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
continue
|
||||
fi
|
||||
|
||||
candidate_version="$(slint_runtime_version "$candidate")"
|
||||
if [[ -n "$SLINT_CURRENT_VERSION" && "$candidate_version" != "$SLINT_CURRENT_VERSION" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "$candidate"
|
||||
return 0
|
||||
done < <(find "$package_root/s/slint" -type f -path "*/lib/$SLINT_NAME" -print 2>/dev/null)
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
SLINT_LIBRARY="$(find_slint_runtime || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "Unable to locate $SLINT_NAME for $EXPECTED_ARCH (current version $SLINT_CURRENT_VERSION)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d "$FRAMEWORKS_DIR"
|
||||
install -m 0755 "$SLINT_LIBRARY" "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
|
||||
install_name_tool -id "@rpath/$SLINT_NAME" "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
if [[ "$SLINT_REFERENCE" != "@rpath/$SLINT_NAME" ]]; then
|
||||
install_name_tool -change "$SLINT_REFERENCE" "@rpath/$SLINT_NAME" "$APP_EXECUTABLE"
|
||||
fi
|
||||
|
||||
if ! otool -l "$APP_EXECUTABLE" | awk '
|
||||
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
|
||||
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
|
||||
END { exit(found ? 0 : 1) }'; then
|
||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
|
||||
fi
|
||||
|
||||
# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code
|
||||
# first, then seal the complete app bundle so local packages remain launchable.
|
||||
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
codesign --force --deep --sign - "$APP_BUNDLE"
|
||||
|
||||
echo "Bundled Slint runtime: $SLINT_LIBRARY"
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/arm64/release/crossdesk"
|
||||
PLATFORM="macos"
|
||||
ARCH="arm64"
|
||||
BINARY_ARCH="arm64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="14.0"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
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"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||
|
||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
VOL_NAME="Install ${APP_NAME_UPPER}"
|
||||
|
||||
echo "delete old files"
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
|
||||
|
||||
mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
|
||||
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
cp "${ICON_PATH}" "${RESOURCES_DIR}/crossedesk.icns"
|
||||
ICON_KEY="<key>CFBundleIconFile</key><string>crossedesk.icns</string>"
|
||||
else
|
||||
ICON_KEY=""
|
||||
fi
|
||||
|
||||
echo "generate Info.plist"
|
||||
cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${IDENTIFIER}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
${ICON_KEY}
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOS_MIN_VERSION}</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>应用需要访问摄像头</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>应用需要访问麦克风</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>应用需要发送 Apple 事件</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>应用需要录屏权限以捕获屏幕内容</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||
find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp "${SCRIPT_DIR}/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..."
|
||||
pkgbuild \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>${INSTALLER_TITLE}</title>
|
||||
<welcome file="welcome.html" mime-type="text/html"/>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="${IDENTIFIER}"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||
<pkg-ref id="${IDENTIFIER}"/>
|
||||
</choice>
|
||||
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
EOF
|
||||
|
||||
productbuild \
|
||||
--distribution build_pkg_temp/Distribution \
|
||||
--package-path build_pkg_temp \
|
||||
--resources build_pkg_resources \
|
||||
"${PKG_NAME}"
|
||||
|
||||
echo "PKG package created: ${PKG_NAME}"
|
||||
|
||||
# Set custom icon for PKG file
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
echo "Setting custom icon for PKG file..."
|
||||
# Create a temporary iconset from icns
|
||||
TEMP_ICON_DIR=$(mktemp -d)
|
||||
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
|
||||
|
||||
# Use sips to create a png from icns for the icon
|
||||
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
|
||||
|
||||
# Method: Use osascript to set file icon (works on macOS)
|
||||
osascript <<APPLESCRIPT
|
||||
use framework "Foundation"
|
||||
use framework "AppKit"
|
||||
|
||||
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
|
||||
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
|
||||
|
||||
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
|
||||
set workspace to current application's NSWorkspace's sharedWorkspace()
|
||||
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
|
||||
APPLESCRIPT
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Custom icon set successfully for ${PKG_NAME}"
|
||||
else
|
||||
echo "Warning: Failed to set custom icon (this is optional)"
|
||||
fi
|
||||
|
||||
rm -rf "${TEMP_ICON_DIR}"
|
||||
fi
|
||||
echo "Set icon finished"
|
||||
|
||||
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||
|
||||
echo "PKG package created successfully."
|
||||
echo "package ${APP_BUNDLE}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="${PROJECT_ROOT}/build/macosx/x86_64/release/crossdesk"
|
||||
PLATFORM="macos"
|
||||
ARCH="x64"
|
||||
BINARY_ARCH="x86_64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="${PROJECT_ROOT}/apps/desktop/resources/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="14.0"
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
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"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||
|
||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
VOL_NAME="Install ${APP_NAME_UPPER}"
|
||||
|
||||
echo "delete old files"
|
||||
rm -rf "${APP_BUNDLE}" "${PKG_NAME}" "${DMG_NAME}" build_pkg_temp CrossDesk_dmg_temp
|
||||
|
||||
mkdir -p build_pkg_temp
|
||||
mkdir -p "${MACOS_DIR}" "${RESOURCES_DIR}"
|
||||
|
||||
cp "${EXECUTABLE_PATH}" "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
chmod +x "${MACOS_DIR}/${APP_NAME_UPPER}"
|
||||
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
cp "${ICON_PATH}" "${RESOURCES_DIR}/crossedesk.icns"
|
||||
ICON_KEY="<key>CFBundleIconFile</key><string>crossedesk.icns</string>"
|
||||
else
|
||||
ICON_KEY=""
|
||||
fi
|
||||
|
||||
echo "generate Info.plist"
|
||||
cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>${IDENTIFIER}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${APP_VERSION}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${APP_NAME_UPPER}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
${ICON_KEY}
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${MACOS_MIN_VERSION}</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>应用需要访问摄像头</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>应用需要访问麦克风</string>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string>应用需要发送 Apple 事件</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>应用需要录屏权限以捕获屏幕内容</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||
find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
"${SCRIPT_DIR}/bundle_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
"${SCRIPT_DIR}/verify_app.sh" "${APP_BUNDLE}" "${BINARY_ARCH}"
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp "${SCRIPT_DIR}/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..."
|
||||
pkgbuild \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>${INSTALLER_TITLE}</title>
|
||||
<welcome file="welcome.html" mime-type="text/html"/>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="${IDENTIFIER}"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||
<pkg-ref id="${IDENTIFIER}"/>
|
||||
</choice>
|
||||
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
EOF
|
||||
|
||||
productbuild \
|
||||
--distribution build_pkg_temp/Distribution \
|
||||
--package-path build_pkg_temp \
|
||||
--resources build_pkg_resources \
|
||||
"${PKG_NAME}"
|
||||
|
||||
echo "PKG package created: ${PKG_NAME}"
|
||||
|
||||
# Set custom icon for PKG file
|
||||
if [ -f "${ICON_PATH}" ]; then
|
||||
echo "Setting custom icon for PKG file..."
|
||||
# Create a temporary iconset from icns
|
||||
TEMP_ICON_DIR=$(mktemp -d)
|
||||
cp "${ICON_PATH}" "${TEMP_ICON_DIR}/icon.icns"
|
||||
|
||||
# Use sips to create a png from icns for the icon
|
||||
sips -s format png "${TEMP_ICON_DIR}/icon.icns" --out "${TEMP_ICON_DIR}/icon.png" 2>/dev/null || true
|
||||
|
||||
# Method: Use osascript to set file icon (works on macOS)
|
||||
osascript <<APPLESCRIPT
|
||||
use framework "Foundation"
|
||||
use framework "AppKit"
|
||||
|
||||
set iconPath to POSIX file "${TEMP_ICON_DIR}/icon.icns"
|
||||
set targetPath to POSIX file "$(pwd)/${PKG_NAME}"
|
||||
|
||||
set iconImage to current application's NSImage's alloc()'s initWithContentsOfFile:(POSIX path of iconPath)
|
||||
set workspace to current application's NSWorkspace's sharedWorkspace()
|
||||
workspace's setIcon:iconImage forFile:(POSIX path of targetPath) options:0
|
||||
APPLESCRIPT
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Custom icon set successfully for ${PKG_NAME}"
|
||||
else
|
||||
echo "Warning: Failed to set custom icon (this is optional)"
|
||||
fi
|
||||
|
||||
rm -rf "${TEMP_ICON_DIR}"
|
||||
fi
|
||||
echo "Set icon finished"
|
||||
|
||||
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||
|
||||
echo "PKG package created successfully."
|
||||
echo "package ${APP_BUNDLE}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
@@ -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,117 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 2 || $# -gt 3 ]]; then
|
||||
echo "Usage: $0 <app-bundle> <expected-architecture> [expected-sdk-version]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
APP_BUNDLE="$1"
|
||||
EXPECTED_ARCH="$2"
|
||||
EXPECTED_SDK_VERSION="${3:-}"
|
||||
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
|
||||
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
|
||||
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
|
||||
|
||||
if [[ ! -x "$APP_EXECUTABLE" ]]; then
|
||||
echo "Missing app executable: $APP_EXECUTABLE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! lipo "$APP_EXECUTABLE" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "Unexpected app architecture; expected $EXPECTED_ARCH" >&2
|
||||
lipo -archs "$APP_EXECUTABLE" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! otool -l "$APP_EXECUTABLE" | awk '
|
||||
$1 == "cmd" { in_rpath = ($2 == "LC_RPATH"); next }
|
||||
in_rpath && $1 == "path" && $2 == "@executable_path/../Frameworks" { found = 1 }
|
||||
END { exit(found ? 0 : 1) }'; then
|
||||
echo "Missing app rpath: @executable_path/../Frameworks" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_dependencies() {
|
||||
local macho="$1"
|
||||
local dependency=""
|
||||
local relative_path=""
|
||||
|
||||
while IFS= read -r dependency; do
|
||||
case "$dependency" in
|
||||
/System/Library/*|/usr/lib/*)
|
||||
;;
|
||||
@rpath/*)
|
||||
relative_path="${dependency#@rpath/}"
|
||||
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
|
||||
echo "Unbundled dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
@executable_path/../Frameworks/*)
|
||||
relative_path="${dependency#@executable_path/../Frameworks/}"
|
||||
if [[ ! -f "$FRAMEWORKS_DIR/$relative_path" ]]; then
|
||||
echo "Unbundled dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
@loader_path/*)
|
||||
relative_path="${dependency#@loader_path/}"
|
||||
if [[ ! -f "$(dirname "$macho")/$relative_path" ]]; then
|
||||
echo "Unbundled dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "External non-system dependency in $macho: $dependency" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
done < <(otool -L "$macho" | tail -n +2 | awk '{ print $1 }')
|
||||
}
|
||||
|
||||
verify_dependencies "$APP_EXECUTABLE"
|
||||
|
||||
SLINT_LIBRARY="$(find "$FRAMEWORKS_DIR" -maxdepth 1 -type f -name 'libslint_cpp*.dylib' -print -quit 2>/dev/null || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "The packaged app does not contain libslint_cpp.dylib" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! lipo "$SLINT_LIBRARY" -verify_arch "$EXPECTED_ARCH" >/dev/null 2>&1; then
|
||||
echo "Unexpected Slint runtime architecture; expected $EXPECTED_ARCH" >&2
|
||||
lipo -archs "$SLINT_LIBRARY" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
verify_dependencies "$SLINT_LIBRARY"
|
||||
|
||||
PLIST_MIN_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$INFO_PLIST")"
|
||||
BINARY_MIN_VERSION="$(otool -l "$APP_EXECUTABLE" | awk '
|
||||
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); version_min = ($2 == "LC_VERSION_MIN_MACOSX"); next }
|
||||
build_version && $1 == "minos" { print $2; exit }
|
||||
version_min && $1 == "version" { print $2; exit }')"
|
||||
if [[ -z "$BINARY_MIN_VERSION" || "$PLIST_MIN_VERSION" != "$BINARY_MIN_VERSION" ]]; then
|
||||
echo "Minimum macOS version mismatch: plist=$PLIST_MIN_VERSION binary=${BINARY_MIN_VERSION:-unknown}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$EXPECTED_SDK_VERSION" ]]; then
|
||||
verify_sdk_version() {
|
||||
local macho="$1"
|
||||
local actual_sdk=""
|
||||
actual_sdk="$(otool -l "$macho" | awk '
|
||||
$1 == "cmd" { build_version = ($2 == "LC_BUILD_VERSION"); next }
|
||||
build_version && $1 == "sdk" { print $2; exit }')"
|
||||
if [[ "$actual_sdk" != "$EXPECTED_SDK_VERSION" ]]; then
|
||||
echo "Unexpected macOS SDK in $macho: expected $EXPECTED_SDK_VERSION, got ${actual_sdk:-unknown}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
verify_sdk_version "$APP_EXECUTABLE"
|
||||
verify_sdk_version "$SLINT_LIBRARY"
|
||||
fi
|
||||
|
||||
codesign --verify --deep --strict --verbose=2 "$APP_BUNDLE"
|
||||
|
||||
echo "Verified $APP_BUNDLE ($EXPECTED_ARCH, macOS $BINARY_MIN_VERSION+)"
|
||||
@@ -0,0 +1,246 @@
|
||||
; Set search path
|
||||
!addincludedir "${__FILEDIR__}"
|
||||
|
||||
; Installer initial constants
|
||||
!define PRODUCT_NAME "CrossDesk"
|
||||
!define PRODUCT_VERSION "${VERSION}"
|
||||
!define PRODUCT_VERSION_NUMERIC "${VERSION_NUMERIC}"
|
||||
!define PRODUCT_PUBLISHER "CrossDesk"
|
||||
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
|
||||
!define APP_NAME "CrossDesk"
|
||||
!define UNINSTALL_REG_KEY "CrossDesk"
|
||||
!define PRODUCT_SERVICE_NAME "CrossDeskService"
|
||||
|
||||
; Installer icon path
|
||||
!define MUI_ICON "${__FILEDIR__}\..\..\resources\windows\crossdesk.ico"
|
||||
|
||||
; Compression settings
|
||||
SetCompressor /FINAL lzma
|
||||
|
||||
; Request admin privileges (needed to write HKLM)
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; ------ MUI Modern UI Definition ------
|
||||
!include "MUI.nsh"
|
||||
!define MUI_ABORTWARNING
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
; Add run-after-install option
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Run ${PRODUCT_NAME}"
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION LaunchApp
|
||||
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_RESERVEFILE_INSTALLOPTIONS
|
||||
; ------ End of MUI Definition ------
|
||||
|
||||
; Include LogicLib for process handling
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||
VIProductVersion "${PRODUCT_VERSION_NUMERIC}"
|
||||
VIAddVersionKey /LANG=2052 "CompanyName" "CrossDesk"
|
||||
VIAddVersionKey /LANG=2052 "FileDescription" "CrossDesk Installer"
|
||||
VIAddVersionKey /LANG=2052 "FileVersion" "${PRODUCT_VERSION}"
|
||||
VIAddVersionKey /LANG=2052 "LegalCopyright" "Copyright (C) CrossDesk contributors"
|
||||
VIAddVersionKey /LANG=2052 "OriginalFilename" "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||
VIAddVersionKey /LANG=2052 "ProductName" "${PRODUCT_NAME}"
|
||||
VIAddVersionKey /LANG=2052 "ProductVersion" "${PRODUCT_VERSION}"
|
||||
InstallDir "$PROGRAMFILES\CrossDesk"
|
||||
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
|
||||
ShowInstDetails show
|
||||
|
||||
Section "MainSection"
|
||||
; Check if CrossDesk is running
|
||||
StrCpy $1 "CrossDesk.exe"
|
||||
|
||||
nsProcess::_FindProcess "$1"
|
||||
Pop $R0
|
||||
${If} $R0 = 0 ;
|
||||
MessageBox MB_ICONQUESTION|MB_YESNO "CrossDesk is running. Do you want to close it and continue the installation?" IDYES closeApp IDNO cancelInstall
|
||||
${Else}
|
||||
Goto installApp
|
||||
${EndIf}
|
||||
|
||||
closeApp:
|
||||
nsProcess::_KillProcess "$1"
|
||||
Pop $R0
|
||||
Sleep 500
|
||||
Goto installApp
|
||||
|
||||
cancelInstall:
|
||||
SetDetailsPrint both
|
||||
MessageBox MB_ICONEXCLAMATION|MB_OK "Installation has been aborted."
|
||||
Abort
|
||||
|
||||
installApp:
|
||||
Call StopInstalledService
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
SetOverwrite ifnewer
|
||||
|
||||
; Main application executable path
|
||||
File /oname=CrossDesk.exe "..\..\..\..\build\windows\x64\release\crossdesk.exe"
|
||||
; Bundle service-side binaries required by the Windows service flow
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_service.exe"
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
||||
; Bundle runtime DLLs from the release output directory
|
||||
File "..\..\..\..\build\windows\x64\release\*.dll"
|
||||
|
||||
Call RegisterInstalledService
|
||||
|
||||
; Write uninstall information
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\CrossDesk.exe"
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}" "NoRepair" 1
|
||||
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "InstallDir" "$INSTDIR"
|
||||
SectionEnd
|
||||
|
||||
Section -AdditionalIcons
|
||||
; Desktop shortcut
|
||||
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
|
||||
|
||||
; Start menu shortcut
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}.lnk" "$INSTDIR\CrossDesk.exe" "" "$INSTDIR\CrossDesk.exe"
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
; Check if CrossDesk is running
|
||||
StrCpy $1 "CrossDesk.exe"
|
||||
|
||||
nsProcess::_FindProcess "$1"
|
||||
Pop $R0
|
||||
${If} $R0 = 0
|
||||
MessageBox MB_ICONQUESTION|MB_YESNO "CrossDesk is running. Do you want to close it and uninstall?" IDYES closeApp IDNO cancelUninstall
|
||||
${Else}
|
||||
Goto uninstallApp
|
||||
${EndIf}
|
||||
|
||||
closeApp:
|
||||
nsProcess::_KillProcess "$1"
|
||||
Pop $R0
|
||||
Sleep 500
|
||||
Goto uninstallApp
|
||||
|
||||
cancelUninstall:
|
||||
SetDetailsPrint both
|
||||
MessageBox MB_ICONEXCLAMATION|MB_OK "Uninstallation has been aborted."
|
||||
Abort
|
||||
|
||||
uninstallApp:
|
||||
Call un.UnregisterInstalledService
|
||||
|
||||
; Delete main executable and uninstaller
|
||||
Delete "$INSTDIR\CrossDesk.exe"
|
||||
Delete "$INSTDIR\crossdesk_service.exe"
|
||||
Delete "$INSTDIR\crossdesk_session_helper.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Recursively delete installation directory
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
; Delete desktop and start menu shortcuts
|
||||
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
|
||||
|
||||
; Delete registry uninstall entry
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINSTALL_REG_KEY}"
|
||||
|
||||
; Delete remembered install dir
|
||||
DeleteRegKey HKCU "Software\${PRODUCT_NAME}"
|
||||
|
||||
; Recursively delete CrossDesk folder in user AppData
|
||||
RMDir /r "$APPDATA\CrossDesk"
|
||||
RMDir /r "$LOCALAPPDATA\CrossDesk"
|
||||
SectionEnd
|
||||
|
||||
; ------ Functions ------
|
||||
Function LaunchApp
|
||||
Exec "$INSTDIR\CrossDesk.exe"
|
||||
FunctionEnd
|
||||
|
||||
Function StopInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc
|
||||
|
||||
DetailPrint "Stopping existing CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
|
||||
${If} $0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
stop_with_sc:
|
||||
DetailPrint "Stopping existing CrossDesk service via Service Control Manager"
|
||||
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
${AndIf} $0 != 1062
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the existing CrossDesk service. The installation will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
Sleep 1500
|
||||
FunctionEnd
|
||||
|
||||
Function RegisterInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 missing_service_binary
|
||||
IfFileExists "$INSTDIR\crossdesk_service.exe" 0 missing_service_binary
|
||||
IfFileExists "$INSTDIR\crossdesk_session_helper.exe" 0 missing_service_binary
|
||||
|
||||
DetailPrint "Registering CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-install' $0
|
||||
${If} $0 != 0
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to register the CrossDesk service. The installation will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
DetailPrint "CrossDesk service registered for on-demand start"
|
||||
|
||||
Return
|
||||
|
||||
missing_service_binary:
|
||||
MessageBox MB_ICONSTOP|MB_OK "CrossDesk service files are missing from the installer package. The installation will be aborted."
|
||||
Abort
|
||||
FunctionEnd
|
||||
|
||||
Function un.UnregisterInstalledService
|
||||
IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc
|
||||
|
||||
DetailPrint "Stopping CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0
|
||||
${If} $0 = 0
|
||||
DetailPrint "Removing CrossDesk service"
|
||||
ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $0
|
||||
${If} $0 = 0
|
||||
Return
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
unregister_with_sc:
|
||||
DetailPrint "Removing CrossDesk service via Service Control Manager"
|
||||
ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
${AndIf} $0 != 1062
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to stop the CrossDesk service. Uninstall will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
Sleep 1500
|
||||
|
||||
ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $0
|
||||
${If} $0 != 0
|
||||
${AndIf} $0 != 1060
|
||||
MessageBox MB_ICONSTOP|MB_OK "Failed to remove the CrossDesk service. Uninstall will be aborted."
|
||||
Abort
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
@@ -0,0 +1,68 @@
|
||||
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
|
||||
}
|
||||
|
||||
function Convert-To-WindowsVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputVersion
|
||||
)
|
||||
|
||||
$body = $InputVersion -replace '^v', ''
|
||||
$baseMatch = [regex]::Match($body, '^[0-9]+(?:\.[0-9]+){0,3}')
|
||||
if (!$baseMatch.Success) {
|
||||
return "0.0.0.0"
|
||||
}
|
||||
|
||||
$parts = [System.Collections.Generic.List[int]]::new()
|
||||
foreach ($part in $baseMatch.Value.Split('.')) {
|
||||
$value = [int]$part
|
||||
if ($value -gt 65535) {
|
||||
$value = 0
|
||||
}
|
||||
$parts.Add($value)
|
||||
}
|
||||
while ($parts.Count -lt 4) {
|
||||
$parts.Add(0)
|
||||
}
|
||||
|
||||
return ($parts -join '.')
|
||||
}
|
||||
|
||||
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
|
||||
$windowsVersion = Convert-To-WindowsVersion -InputVersion $normalizedVersion
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Push-Location $scriptDir
|
||||
try {
|
||||
& makensis "/DVERSION=$normalizedVersion" "/DVERSION_NUMERIC=$windowsVersion" "nsis_script.nsi"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<parameters>
|
||||
<parameter name="version" required="true" />
|
||||
</parameters>
|
||||
<pe-file path="crossdesk-win-x64-${version}.exe"
|
||||
product-name="CrossDesk"
|
||||
product-version="${version}"
|
||||
file-version="${version}"
|
||||
company-name="CrossDesk"
|
||||
copyright="Copyright (C) CrossDesk contributors"
|
||||
original-filename="crossdesk-win-x64-${version}.exe">
|
||||
<authenticode-sign />
|
||||
</pe-file>
|
||||
</artifact-configuration>
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "daemon.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "platform/daemon_backend.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kRestartDelayMs = 1000;
|
||||
|
||||
} // namespace
|
||||
|
||||
Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
|
||||
|
||||
void Daemon::stop() {
|
||||
running_.store(false);
|
||||
crossdesk::platform::RequestDaemonStop();
|
||||
}
|
||||
|
||||
bool Daemon::isRunning() const {
|
||||
return running_.load() && !crossdesk::platform::DaemonStopRequested();
|
||||
}
|
||||
|
||||
bool Daemon::start(MainLoopFunc loop) {
|
||||
crossdesk::platform::ResetDaemonStop();
|
||||
if (!crossdesk::platform::PrepareDaemon()) return false;
|
||||
running_.store(true);
|
||||
return runWithRestart(std::move(loop));
|
||||
}
|
||||
|
||||
bool Daemon::runWithRestart(MainLoopFunc loop) {
|
||||
int restart_count = 0;
|
||||
const std::string executable_path =
|
||||
crossdesk::platform::GetDaemonExecutablePath();
|
||||
if (executable_path.empty()) {
|
||||
std::cerr
|
||||
<< "Failed to get executable path, falling back to direct execution"
|
||||
<< std::endl;
|
||||
while (isRunning()) {
|
||||
try {
|
||||
loop();
|
||||
break;
|
||||
} catch (...) {
|
||||
++restart_count;
|
||||
std::cerr << "Exception caught, restarting... (attempt "
|
||||
<< restart_count << ")" << std::endl;
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(kRestartDelayMs));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
while (isRunning()) {
|
||||
const crossdesk::platform::DaemonChildResult result =
|
||||
crossdesk::platform::RunDaemonChild(
|
||||
executable_path, [this] { return isRunning(); });
|
||||
if (!isRunning() || result.normal_exit) break;
|
||||
|
||||
++restart_count;
|
||||
if (!result.started) {
|
||||
std::cerr << "Failed to create child process";
|
||||
} else if (result.terminated_by_signal) {
|
||||
std::cerr << "Child process crashed with signal " << result.exit_code;
|
||||
} else {
|
||||
std::cerr << "Child process exited with code " << result.exit_code;
|
||||
}
|
||||
std::cerr << ", restarting... (attempt " << restart_count << ")"
|
||||
<< std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-11-19
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DAEMON_H_
|
||||
#define _DAEMON_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
class Daemon {
|
||||
public:
|
||||
using MainLoopFunc = std::function<void()>;
|
||||
|
||||
Daemon(const std::string& name);
|
||||
|
||||
bool start(MainLoopFunc loop);
|
||||
|
||||
void stop();
|
||||
|
||||
bool isRunning() const;
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
bool runWithRestart(MainLoopFunc loop);
|
||||
|
||||
std::atomic<bool> running_;
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,231 @@
|
||||
#ifdef _WIN32
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
#pragma comment(linker, "/subsystem:\"console\"")
|
||||
#else
|
||||
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <cstdio>
|
||||
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
#include "config_center.h"
|
||||
#include "daemon.h"
|
||||
#include "path_manager.h"
|
||||
#include "render.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
|
||||
void EnsureConsoleForCli() {
|
||||
static bool console_ready = false;
|
||||
if (console_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
DWORD error = GetLastError();
|
||||
if (error != ERROR_ACCESS_DENIED) {
|
||||
AllocConsole();
|
||||
}
|
||||
}
|
||||
|
||||
FILE* stream = nullptr;
|
||||
freopen_s(&stream, "CONOUT$", "w", stdout);
|
||||
freopen_s(&stream, "CONOUT$", "w", stderr);
|
||||
freopen_s(&stream, "CONIN$", "r", stdin);
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
console_ready = true;
|
||||
}
|
||||
|
||||
void PrintServiceCliUsage() {
|
||||
std::cout
|
||||
<< "CrossDesk service management commands\n"
|
||||
<< " --service-install Install the sibling crossdesk_service.exe\n"
|
||||
<< " --service-uninstall Remove the installed Windows service\n"
|
||||
<< " --service-start Start the Windows service\n"
|
||||
<< " --service-stop Stop the Windows service\n"
|
||||
<< " --service-sas Ask the service to send Secure Attention "
|
||||
"Sequence\n"
|
||||
<< " --service-ping Ping the service over named pipe IPC\n"
|
||||
<< " --service-status Query service runtime status\n"
|
||||
<< " --service-help Show this help\n";
|
||||
}
|
||||
|
||||
std::wstring GetCurrentExecutablePathW() {
|
||||
wchar_t path[MAX_PATH] = {0};
|
||||
DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (length == 0 || length >= MAX_PATH) {
|
||||
return L"";
|
||||
}
|
||||
return std::wstring(path, length);
|
||||
}
|
||||
|
||||
std::filesystem::path GetSiblingServiceExecutablePath() {
|
||||
std::wstring current_executable = GetCurrentExecutablePathW();
|
||||
if (current_executable.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return std::filesystem::path(current_executable).parent_path() /
|
||||
L"crossdesk_service.exe";
|
||||
}
|
||||
|
||||
bool IsServiceCliCommand(const char* arg) {
|
||||
if (arg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return std::strcmp(arg, "--service-install") == 0 ||
|
||||
std::strcmp(arg, "--service-uninstall") == 0 ||
|
||||
std::strcmp(arg, "--service-start") == 0 ||
|
||||
std::strcmp(arg, "--service-stop") == 0 ||
|
||||
std::strcmp(arg, "--service-sas") == 0 ||
|
||||
std::strcmp(arg, "--service-ping") == 0 ||
|
||||
std::strcmp(arg, "--service-status") == 0 ||
|
||||
std::strcmp(arg, "--service-help") == 0;
|
||||
}
|
||||
|
||||
void TryStartManagedWindowsService() {
|
||||
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
|
||||
if (service_path.empty() || !std::filesystem::exists(service_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!crossdesk::IsCrossDeskServiceInstalled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
crossdesk::StartCrossDeskService();
|
||||
}
|
||||
|
||||
int HandleServiceCliCommand(const std::string& command) {
|
||||
EnsureConsoleForCli();
|
||||
|
||||
if (command == "--service-help") {
|
||||
PrintServiceCliUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-install") {
|
||||
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
|
||||
if (service_path.empty()) {
|
||||
std::cerr << "Failed to locate crossdesk_service.exe" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (!std::filesystem::exists(service_path)) {
|
||||
std::cerr << "Service binary not found: " << service_path.string()
|
||||
<< std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool success = crossdesk::InstallCrossDeskService(service_path.wstring());
|
||||
std::cout << (success ? "install ok" : "install failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-uninstall") {
|
||||
bool success = crossdesk::UninstallCrossDeskService();
|
||||
std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-start") {
|
||||
bool success = crossdesk::StartCrossDeskService();
|
||||
std::cout << (success ? "start ok" : "start failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-stop") {
|
||||
bool success = crossdesk::StopCrossDeskService();
|
||||
std::cout << (success ? "stop ok" : "stop failed") << std::endl;
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
if (command == "--service-sas") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-ping") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (command == "--service-status") {
|
||||
std::cout << crossdesk::QueryCrossDeskService("status") << std::endl;
|
||||
return 0;
|
||||
}
|
||||
|
||||
PrintServiceCliUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef _WIN32
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
#endif
|
||||
|
||||
// check if running as child process
|
||||
bool is_child = false;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--child") == 0) {
|
||||
is_child = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_child) {
|
||||
// child process: run render directly
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
TryStartManagedWindowsService();
|
||||
#endif
|
||||
|
||||
bool enable_daemon = false;
|
||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||
if (path_manager) {
|
||||
std::string cache_path = path_manager->GetCachePath().string();
|
||||
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
|
||||
enable_daemon = config_center.IsEnableDaemon();
|
||||
}
|
||||
|
||||
if (enable_daemon) {
|
||||
// start daemon with restart monitoring
|
||||
Daemon daemon("CrossDesk");
|
||||
|
||||
// define main loop function: run render and stop daemon on normal exit
|
||||
Daemon::MainLoopFunc main_loop = [&daemon]() {
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
daemon.stop();
|
||||
};
|
||||
|
||||
// start daemon and return result
|
||||
bool success = daemon.start(main_loop);
|
||||
return success ? 0 : 1;
|
||||
}
|
||||
|
||||
// run without daemon: direct execution
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "autostart.h"
|
||||
|
||||
#include "platform/autostart_backend.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool EnableAutostart(const std::string& app_name) {
|
||||
const std::string executable_path = platform::GetAutostartExecutablePath();
|
||||
return !executable_path.empty() &&
|
||||
platform::EnableAutostart(app_name, executable_path);
|
||||
}
|
||||
|
||||
bool DisableAutostart(const std::string& app_name) {
|
||||
return platform::DisableAutostart(app_name);
|
||||
}
|
||||
|
||||
bool IsAutostartEnabled(const std::string& app_name) {
|
||||
return platform::IsAutostartEnabled(app_name);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-11-18
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _AUTOSTART_H_
|
||||
#define _AUTOSTART_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool EnableAutostart(const std::string& appName);
|
||||
|
||||
bool DisableAutostart(const std::string& appName);
|
||||
|
||||
bool IsAutostartEnabled(const std::string& appName);
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2025-05-15
|
||||
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DISPLAY_INFO_H_
|
||||
#define _DISPLAY_INFO_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class DisplayInfo {
|
||||
public:
|
||||
DisplayInfo(std::string name, int left, int top, int right, int bottom)
|
||||
: name(name), left(left), top(top), right(right), bottom(bottom) {
|
||||
width = right - left;
|
||||
height = bottom - top;
|
||||
}
|
||||
DisplayInfo(void* handle, std::string name, bool is_primary, int left,
|
||||
int top, int right, int bottom)
|
||||
: handle(handle),
|
||||
name(name),
|
||||
is_primary(is_primary),
|
||||
left(left),
|
||||
top(top),
|
||||
right(right),
|
||||
bottom(bottom) {
|
||||
width = right - left;
|
||||
height = bottom - top;
|
||||
}
|
||||
~DisplayInfo() {}
|
||||
|
||||
void* handle = nullptr;
|
||||
std::string name = "";
|
||||
bool is_primary = false;
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
int right = 0;
|
||||
int bottom = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
|
||||
#define CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline std::filesystem::path PathFromUtf8(std::string_view value) {
|
||||
#if defined(__cpp_char8_t)
|
||||
const auto *begin = reinterpret_cast<const char8_t *>(value.data());
|
||||
return std::filesystem::path(std::u8string(begin, begin + value.size()));
|
||||
#else
|
||||
return std::filesystem::u8path(value.begin(), value.end());
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_FILESYSTEM_UTF8_H_
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "nv12_scaler.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
|
||||
#include "libyuv/planar_functions.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
|
||||
const uint8_t* src_uv, int src_stride_uv, int src_width,
|
||||
int src_height, uint8_t* dst_y, int dst_stride_y,
|
||||
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
|
||||
int dst_height, libyuv::FilterMode filtering,
|
||||
std::vector<uint8_t>* scratch_buffer) {
|
||||
if (!src_y || !src_uv || !dst_y || !dst_uv || !scratch_buffer ||
|
||||
src_width <= 0 || src_height <= 0 || dst_width <= 0 || dst_height <= 0 ||
|
||||
(src_width & 1) != 0 || (src_height & 1) != 0 || (dst_width & 1) != 0 ||
|
||||
(dst_height & 1) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const int src_chroma_width = src_width / 2;
|
||||
const int src_chroma_height = src_height / 2;
|
||||
const int dst_chroma_width = dst_width / 2;
|
||||
const int dst_chroma_height = dst_height / 2;
|
||||
const size_t src_chroma_size =
|
||||
static_cast<size_t>(src_chroma_width) * src_chroma_height;
|
||||
const size_t dst_chroma_size =
|
||||
static_cast<size_t>(dst_chroma_width) * dst_chroma_height;
|
||||
const size_t max_size = std::numeric_limits<size_t>::max();
|
||||
if (src_chroma_size > max_size / 2 || dst_chroma_size > max_size / 2) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const size_t src_planes_size = 2 * src_chroma_size;
|
||||
const size_t dst_planes_size = 2 * dst_chroma_size;
|
||||
if (src_planes_size > max_size - dst_planes_size) {
|
||||
return -1;
|
||||
}
|
||||
scratch_buffer->resize(src_planes_size + dst_planes_size);
|
||||
uint8_t* src_u = scratch_buffer->data();
|
||||
uint8_t* src_v = src_u + src_chroma_size;
|
||||
uint8_t* dst_u = src_v + src_chroma_size;
|
||||
uint8_t* dst_v = dst_u + dst_chroma_size;
|
||||
|
||||
libyuv::SplitUVPlane(src_uv, src_stride_uv, src_u, src_chroma_width, src_v,
|
||||
src_chroma_width, src_chroma_width, src_chroma_height);
|
||||
const int result = libyuv::I420Scale(
|
||||
src_y, src_stride_y, src_u, src_chroma_width, src_v, src_chroma_width,
|
||||
src_width, src_height, dst_y, dst_stride_y, dst_u, dst_chroma_width,
|
||||
dst_v, dst_chroma_width, dst_width, dst_height, filtering);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
libyuv::MergeUVPlane(dst_u, dst_chroma_width, dst_v, dst_chroma_width, dst_uv,
|
||||
dst_stride_uv, dst_chroma_width, dst_chroma_height);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef CROSSDESK_COMMON_NV12_SCALER_H_
|
||||
#define CROSSDESK_COMMON_NV12_SCALER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "libyuv/scale.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Scales NV12 by separating the interleaved chroma plane before filtering.
|
||||
// This avoids the downward chroma rounding bias in libyuv::NV12Scale while
|
||||
// preserving the requested filtering quality.
|
||||
int ScaleNv12ViaI420(const uint8_t* src_y, int src_stride_y,
|
||||
const uint8_t* src_uv, int src_stride_uv, int src_width,
|
||||
int src_height, uint8_t* dst_y, int dst_stride_y,
|
||||
uint8_t* dst_uv, int dst_stride_uv, int dst_width,
|
||||
int dst_height, libyuv::FilterMode filtering,
|
||||
std::vector<uint8_t>* scratch_buffer);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_NV12_SCALER_H_
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2023-12-18
|
||||
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PLATFORM_H_
|
||||
#define _PLATFORM_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
std::string GetMac();
|
||||
std::string GetHostName();
|
||||
bool IsWaylandSession();
|
||||
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "rounded_corner_button.h"
|
||||
|
||||
namespace crossdesk {
|
||||
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
|
||||
ImDrawFlags round_flags, bool enabled,
|
||||
ImU32 normal_col, ImU32 hover_col, ImU32 active_col,
|
||||
ImU32 border_col) {
|
||||
ImGuiWindow* current_window = ImGui::GetCurrentWindow();
|
||||
if (current_window->SkipItems) return false;
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
|
||||
ImGuiID button_id = current_window->GetID(label);
|
||||
ImVec2 cursor_pos = current_window->DC.CursorPos;
|
||||
ImVec2 button_size = ImGui::CalcItemSize(size, 0.0f, 0.0f);
|
||||
ImRect button_rect(cursor_pos, ImVec2(cursor_pos.x + button_size.x,
|
||||
cursor_pos.y + button_size.y));
|
||||
ImGui::ItemSize(button_rect);
|
||||
if (!ImGui::ItemAdd(button_rect, button_id)) return false;
|
||||
|
||||
bool is_hovered = false, is_held = false;
|
||||
bool is_pressed = false;
|
||||
if (enabled) {
|
||||
is_pressed =
|
||||
ImGui::ButtonBehavior(button_rect, button_id, &is_hovered, &is_held);
|
||||
}
|
||||
|
||||
if (normal_col == 0) normal_col = ImGui::GetColorU32(ImGuiCol_Button);
|
||||
if (hover_col == 0) hover_col = ImGui::GetColorU32(ImGuiCol_ButtonHovered);
|
||||
if (active_col == 0) active_col = ImGui::GetColorU32(ImGuiCol_ButtonActive);
|
||||
if (border_col == 0) border_col = ImGui::GetColorU32(ImGuiCol_Border);
|
||||
|
||||
ImU32 fill_color = normal_col;
|
||||
if (is_held && is_hovered)
|
||||
fill_color = active_col;
|
||||
else if (is_hovered)
|
||||
fill_color = hover_col;
|
||||
|
||||
if (!enabled) fill_color = IM_COL32(120, 120, 120, 180);
|
||||
|
||||
ImDrawList* window_draw_list = ImGui::GetWindowDrawList();
|
||||
|
||||
window_draw_list->AddRectFilled(button_rect.Min, button_rect.Max, fill_color,
|
||||
rounding, round_flags);
|
||||
|
||||
if (style.FrameBorderSize > 0.0f) {
|
||||
window_draw_list->AddRect(button_rect.Min, button_rect.Max, border_col,
|
||||
rounding, round_flags, style.FrameBorderSize);
|
||||
}
|
||||
|
||||
ImU32 text_color =
|
||||
ImGui::GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled);
|
||||
|
||||
const char* label_end = ImGui::FindRenderedTextEnd(label);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImGui::ColorConvertU32ToFloat4(text_color));
|
||||
ImGui::RenderTextClipped(button_rect.Min, button_rect.Max, label, label_end,
|
||||
nullptr, ImVec2(0.5f, 0.5f), &button_rect);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
return is_pressed;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-26
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _ROUNDED_CORNER_BUTTON_H_
|
||||
#define _ROUNDED_CORNER_BUTTON_H_
|
||||
|
||||
#include "imgui.h"
|
||||
#include "imgui_internal.h"
|
||||
|
||||
namespace crossdesk {
|
||||
bool RoundedCornerButton(const char* label, const ImVec2& size, float rounding,
|
||||
ImDrawFlags round_flags, bool enabled = true,
|
||||
ImU32 normal_col = 0, ImU32 hover_col = 0,
|
||||
ImU32 active_col = 0, ImU32 border_col = 0);
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "shared_cursor_state.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::mutex g_cursor_state_mutex;
|
||||
SharedCursorState g_cursor_state;
|
||||
bool g_cursor_state_available = false;
|
||||
|
||||
} // namespace
|
||||
|
||||
void PublishSharedCursorState(bool visible, RemoteCursorShape shape) {
|
||||
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
|
||||
++g_cursor_state.generation;
|
||||
g_cursor_state.visible = visible;
|
||||
g_cursor_state.shape = visible ? shape : RemoteCursorShape::none;
|
||||
g_cursor_state_available = true;
|
||||
}
|
||||
|
||||
bool GetSharedCursorState(SharedCursorState* state) {
|
||||
if (!state) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
|
||||
if (!g_cursor_state_available) {
|
||||
return false;
|
||||
}
|
||||
*state = g_cursor_state;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearSharedCursorState() {
|
||||
std::lock_guard<std::mutex> lock(g_cursor_state_mutex);
|
||||
++g_cursor_state.generation;
|
||||
g_cursor_state.visible = false;
|
||||
g_cursor_state.shape = RemoteCursorShape::none;
|
||||
g_cursor_state_available = false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
|
||||
#define CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <remote_cursor_shape.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct SharedCursorState {
|
||||
uint64_t generation = 0;
|
||||
bool visible = false;
|
||||
RemoteCursorShape shape = RemoteCursorShape::default_cursor;
|
||||
};
|
||||
|
||||
void PublishSharedCursorState(bool visible, RemoteCursorShape shape);
|
||||
bool GetSharedCursorState(SharedCursorState* state);
|
||||
void ClearSharedCursorState();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_SHARED_CURSOR_STATE_H_
|
||||
@@ -0,0 +1,460 @@
|
||||
#include "config_center.h"
|
||||
|
||||
#include "autostart.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
bool IsValidTurnModeValue(long value) {
|
||||
return value >= static_cast<long>(ConfigCenter::TURN_MODE::DISABLED) &&
|
||||
value <= static_cast<long>(ConfigCenter::TURN_MODE::FORCE_TCP);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ConfigCenter::ConfigCenter(const std::string& config_path)
|
||||
: config_path_(config_path) {
|
||||
ini_.SetUnicode(true);
|
||||
Load();
|
||||
}
|
||||
|
||||
ConfigCenter::~ConfigCenter() {}
|
||||
|
||||
int ConfigCenter::Load() {
|
||||
SI_Error rc = ini_.LoadFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
Save();
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool persist_config_migration = false;
|
||||
persist_config_migration |= ini_.Delete(section_, "video_content_type");
|
||||
persist_config_migration |= ini_.Delete(section_, "screen_content");
|
||||
persist_config_migration |=
|
||||
ini_.Delete(section_, "enable_desktop_quality_optimization");
|
||||
persist_config_migration |=
|
||||
ini_.Delete(section_, "enable_minimize_to_tray");
|
||||
|
||||
const long language_value =
|
||||
ini_.GetLongValue(section_, "language", static_cast<long>(language_));
|
||||
if (language_value < static_cast<long>(LANGUAGE::CHINESE) ||
|
||||
language_value > static_cast<long>(LANGUAGE::RUSSIAN)) {
|
||||
language_ = LANGUAGE::ENGLISH;
|
||||
} else {
|
||||
language_ = static_cast<LANGUAGE>(language_value);
|
||||
}
|
||||
|
||||
video_quality_ = static_cast<VIDEO_QUALITY>(ini_.GetLongValue(
|
||||
section_, "video_quality", static_cast<long>(video_quality_)));
|
||||
|
||||
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
|
||||
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
|
||||
|
||||
video_encode_format_ = static_cast<VIDEO_ENCODE_FORMAT>(
|
||||
ini_.GetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_)));
|
||||
|
||||
hardware_video_codec_ = ini_.GetBoolValue(section_, "hardware_video_codec",
|
||||
hardware_video_codec_);
|
||||
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_config_migration = true;
|
||||
}
|
||||
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
enable_self_hosted_ =
|
||||
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
const char* signal_server_host_value =
|
||||
ini_.GetValue(section_, "signal_server_host", nullptr);
|
||||
if (signal_server_host_value != nullptr &&
|
||||
strlen(signal_server_host_value) > 0) {
|
||||
signal_server_host_ = signal_server_host_value;
|
||||
} else {
|
||||
signal_server_host_ = "";
|
||||
}
|
||||
const char* signal_server_port_value =
|
||||
ini_.GetValue(section_, "signal_server_port", nullptr);
|
||||
if (signal_server_port_value != nullptr &&
|
||||
strlen(signal_server_port_value) > 0) {
|
||||
signal_server_port_ =
|
||||
static_cast<int>(ini_.GetLongValue(section_, "signal_server_port", 0));
|
||||
} else {
|
||||
signal_server_port_ = 0;
|
||||
}
|
||||
const char* coturn_server_port_value =
|
||||
ini_.GetValue(section_, "coturn_server_port", nullptr);
|
||||
if (coturn_server_port_value != nullptr &&
|
||||
strlen(coturn_server_port_value) > 0) {
|
||||
coturn_server_port_ =
|
||||
static_cast<int>(ini_.GetLongValue(section_, "coturn_server_port", 0));
|
||||
} else {
|
||||
coturn_server_port_ = 0;
|
||||
}
|
||||
|
||||
enable_autostart_ =
|
||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
const char* file_transfer_save_path_value =
|
||||
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
|
||||
if (file_transfer_save_path_value != nullptr &&
|
||||
strlen(file_transfer_save_path_value) > 0) {
|
||||
file_transfer_save_path_ = file_transfer_save_path_value;
|
||||
} else {
|
||||
file_transfer_save_path_ = "";
|
||||
}
|
||||
|
||||
if (persist_config_migration && ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::Save() {
|
||||
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
|
||||
ini_.SetLongValue(section_, "video_quality",
|
||||
static_cast<long>(video_quality_));
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
// only save when self hosted
|
||||
if (enable_self_hosted_) {
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
}
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// setters
|
||||
|
||||
int ConfigCenter::SetLanguage(LANGUAGE language) {
|
||||
language_ = language;
|
||||
ini_.SetLongValue(section_, "language", static_cast<long>(language_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoQuality(VIDEO_QUALITY video_quality) {
|
||||
video_quality_ = video_quality;
|
||||
ini_.SetLongValue(section_, "video_quality",
|
||||
static_cast<long>(video_quality_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
|
||||
video_frame_rate_ = video_frame_rate;
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoEncodeFormat(
|
||||
VIDEO_ENCODE_FORMAT video_encode_format) {
|
||||
video_encode_format_ = video_encode_format;
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
static_cast<long>(video_encode_format_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetHardwareVideoCodec(bool hardware_video_codec) {
|
||||
hardware_video_codec_ = hardware_video_codec;
|
||||
ini_.SetBoolValue(section_, "hardware_video_codec", hardware_video_codec_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurnMode(TURN_MODE turn_mode) {
|
||||
if (!IsValidTurnModeValue(static_cast<long>(turn_mode))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
turn_mode_ = turn_mode;
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetTurn(bool enable_turn) {
|
||||
if (!enable_turn) {
|
||||
return SetTurnMode(TURN_MODE::DISABLED);
|
||||
}
|
||||
if (turn_mode_ == TURN_MODE::DISABLED) {
|
||||
return SetTurnMode(TURN_MODE::AUTO_UDP_TCP);
|
||||
}
|
||||
return SetTurnMode(turn_mode_);
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSrtp(bool enable_srtp) {
|
||||
enable_srtp_ = enable_srtp;
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
|
||||
signal_server_host_ = signal_server_host;
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetServerPort(int signal_server_port) {
|
||||
signal_server_port_ = signal_server_port;
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetCoturnServerPort(int coturn_server_port) {
|
||||
coturn_server_port_ = coturn_server_port;
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSelfHosted(bool enable_self_hosted) {
|
||||
enable_self_hosted_ = enable_self_hosted;
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
// load from config if self hosted is enabled
|
||||
if (enable_self_hosted_) {
|
||||
const char* signal_server_host_value =
|
||||
ini_.GetValue(section_, "signal_server_host", nullptr);
|
||||
if (signal_server_host_value != nullptr &&
|
||||
strlen(signal_server_host_value) > 0) {
|
||||
signal_server_host_ = signal_server_host_value;
|
||||
}
|
||||
const char* signal_server_port_value =
|
||||
ini_.GetValue(section_, "signal_server_port", nullptr);
|
||||
if (signal_server_port_value != nullptr &&
|
||||
strlen(signal_server_port_value) > 0) {
|
||||
signal_server_port_ = static_cast<int>(
|
||||
ini_.GetLongValue(section_, "signal_server_port", 0));
|
||||
}
|
||||
const char* coturn_server_port_value =
|
||||
ini_.GetValue(section_, "coturn_server_port", nullptr);
|
||||
if (coturn_server_port_value != nullptr &&
|
||||
strlen(coturn_server_port_value) > 0) {
|
||||
coturn_server_port_ = static_cast<int>(
|
||||
ini_.GetLongValue(section_, "coturn_server_port", 0));
|
||||
}
|
||||
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
ini_.SetLongValue(section_, "signal_server_port",
|
||||
static_cast<long>(signal_server_port_));
|
||||
ini_.SetLongValue(section_, "coturn_server_port",
|
||||
static_cast<long>(coturn_server_port_));
|
||||
}
|
||||
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetAutostart(bool enable_autostart) {
|
||||
enable_autostart_ = enable_autostart;
|
||||
bool success = false;
|
||||
if (enable_autostart) {
|
||||
success = EnableAutostart("CrossDesk");
|
||||
} else {
|
||||
success = DisableAutostart("CrossDesk");
|
||||
}
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
LOG_ERROR("SetAutostart failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetDaemon(bool enable_daemon) {
|
||||
enable_daemon_ = enable_daemon;
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
|
||||
ConfigCenter::VIDEO_QUALITY ConfigCenter::GetVideoQuality() const {
|
||||
return video_quality_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
|
||||
return video_frame_rate_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsHardwareVideoCodec() const {
|
||||
return hardware_video_codec_;
|
||||
}
|
||||
|
||||
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_; }
|
||||
|
||||
std::string ConfigCenter::GetSignalServerHost() const {
|
||||
return signal_server_host_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetSignalServerPort() const { return signal_server_port_; }
|
||||
|
||||
int ConfigCenter::GetCoturnServerPort() const { return coturn_server_port_; }
|
||||
|
||||
std::string ConfigCenter::GetDefaultServerHost() const {
|
||||
return signal_server_host_default_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetDefaultSignalServerPort() const {
|
||||
return server_port_default_;
|
||||
}
|
||||
|
||||
int ConfigCenter::GetDefaultCoturnServerPort() const {
|
||||
return coturn_server_port_default_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
|
||||
|
||||
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
file_transfer_save_path_ = path;
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string ConfigCenter::GetFileTransferSavePath() const {
|
||||
return file_transfer_save_path_;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _CONFIG_CENTER_H_
|
||||
#define _CONFIG_CENTER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "SimpleIni.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class ConfigCenter {
|
||||
public:
|
||||
enum class LANGUAGE { CHINESE = 0, ENGLISH = 1, RUSSIAN = 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_ENCODE_FORMAT { H264 = 0, AV1 = 1 };
|
||||
enum class TURN_MODE {
|
||||
DISABLED = 0,
|
||||
AUTO_UDP_TCP = 1,
|
||||
FORCE_UDP = 2,
|
||||
FORCE_TCP = 3
|
||||
};
|
||||
|
||||
public:
|
||||
explicit ConfigCenter(const std::string& config_path = "config.ini");
|
||||
~ConfigCenter();
|
||||
|
||||
// write config
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetVideoQuality(VIDEO_QUALITY video_quality);
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
int SetTurn(bool enable_turn);
|
||||
int SetSrtp(bool enable_srtp);
|
||||
int SetServerHost(const std::string& signal_server_host);
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetCoturnServerPort(int coturn_server_port);
|
||||
int SetSelfHosted(bool enable_self_hosted);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
// read config
|
||||
|
||||
LANGUAGE GetLanguage() const;
|
||||
VIDEO_QUALITY GetVideoQuality() const;
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
bool IsEnableTurn() const;
|
||||
bool IsEnableSrtp() const;
|
||||
std::string GetSignalServerHost() const;
|
||||
int GetSignalServerPort() const;
|
||||
int GetCoturnServerPort() const;
|
||||
std::string GetDefaultServerHost() const;
|
||||
int GetDefaultSignalServerPort() const;
|
||||
int GetDefaultCoturnServerPort() const;
|
||||
bool IsSelfHosted() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
int Load();
|
||||
int Save();
|
||||
|
||||
private:
|
||||
std::string config_path_;
|
||||
CSimpleIniA ini_;
|
||||
const char* section_ = "Settings";
|
||||
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
bool enable_srtp_ = false;
|
||||
std::string signal_server_host_ = "";
|
||||
std::string signal_server_host_default_ = "api.crossdesk.cn";
|
||||
int signal_server_port_ = 0;
|
||||
int server_port_default_ = 9099;
|
||||
int coturn_server_port_ = 0;
|
||||
int coturn_server_port_default_ = 3478;
|
||||
bool enable_self_hosted_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2023-12-14
|
||||
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_CONTROLLER_H_
|
||||
#define _DEVICE_CONTROLLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "display_info.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// int key_code, bool is_down, uint32_t scan_code, bool extended
|
||||
using OnKeyAction = void (*)(int, bool, uint32_t, bool, void*);
|
||||
|
||||
class DeviceController {
|
||||
public:
|
||||
virtual ~DeviceController() = default;
|
||||
};
|
||||
|
||||
class MouseController : public DeviceController {
|
||||
public:
|
||||
~MouseController() override = default;
|
||||
|
||||
virtual int Init(std::vector<DisplayInfo> display_info_list) = 0;
|
||||
virtual int Destroy() = 0;
|
||||
virtual int SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) = 0;
|
||||
virtual void UpdateDisplayInfoList(
|
||||
const std::vector<DisplayInfo>&) {}
|
||||
};
|
||||
|
||||
class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
~KeyboardCapturer() override = default;
|
||||
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr) = 0;
|
||||
virtual int Unhook() = 0;
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false) = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2023-12-14
|
||||
* Copyright (c) 2023 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _DEVICE_CONTROLLER_FACTORY_H_
|
||||
#define _DEVICE_CONTROLLER_FACTORY_H_
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class DeviceControllerFactory {
|
||||
public:
|
||||
enum Device { Mouse = 0, Keyboard };
|
||||
|
||||
public:
|
||||
virtual ~DeviceControllerFactory() {}
|
||||
|
||||
public:
|
||||
DeviceController* Create(Device device);
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,748 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-11-25
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _KEYBOARD_CONVERTER_H_
|
||||
#define _KEYBOARD_CONVERTER_H_
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Windows vkCode to macOS CGKeyCode (104 keys)
|
||||
std::map<int, int> vkCodeToCGKeyCode = {
|
||||
// A-Z
|
||||
{0x41, 0x00}, // A
|
||||
{0x42, 0x0B}, // B
|
||||
{0x43, 0x08}, // C
|
||||
{0x44, 0x02}, // D
|
||||
{0x45, 0x0E}, // E
|
||||
{0x46, 0x03}, // F
|
||||
{0x47, 0x05}, // G
|
||||
{0x48, 0x04}, // H
|
||||
{0x49, 0x22}, // I
|
||||
{0x4A, 0x26}, // J
|
||||
{0x4B, 0x28}, // K
|
||||
{0x4C, 0x25}, // L
|
||||
{0x4D, 0x2E}, // M
|
||||
{0x4E, 0x2D}, // N
|
||||
{0x4F, 0x1F}, // O
|
||||
{0x50, 0x23}, // P
|
||||
{0x51, 0x0C}, // Q
|
||||
{0x52, 0x0F}, // R
|
||||
{0x53, 0x01}, // S
|
||||
{0x54, 0x11}, // T
|
||||
{0x55, 0x20}, // U
|
||||
{0x56, 0x09}, // V
|
||||
{0x57, 0x0D}, // W
|
||||
{0x58, 0x07}, // X
|
||||
{0x59, 0x10}, // Y
|
||||
{0x5A, 0x06}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x30, 0x1D}, // 0
|
||||
{0x31, 0x12}, // 1
|
||||
{0x32, 0x13}, // 2
|
||||
{0x33, 0x14}, // 3
|
||||
{0x34, 0x15}, // 4
|
||||
{0x35, 0x17}, // 5
|
||||
{0x36, 0x16}, // 6
|
||||
{0x37, 0x1A}, // 7
|
||||
{0x38, 0x1C}, // 8
|
||||
{0x39, 0x19}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x70, 0x7A}, // F1
|
||||
{0x71, 0x78}, // F2
|
||||
{0x72, 0x63}, // F3
|
||||
{0x73, 0x76}, // F4
|
||||
{0x74, 0x60}, // F5
|
||||
{0x75, 0x61}, // F6
|
||||
{0x76, 0x62}, // F7
|
||||
{0x77, 0x64}, // F8
|
||||
{0x78, 0x65}, // F9
|
||||
{0x79, 0x6D}, // F10
|
||||
{0x7A, 0x67}, // F11
|
||||
{0x7B, 0x6F}, // F12
|
||||
|
||||
// control keys
|
||||
{0x1B, 0x35}, // Escape
|
||||
{0x0D, 0x24}, // Enter
|
||||
{0x20, 0x31}, // Space
|
||||
{0x08, 0x33}, // Backspace
|
||||
{0x09, 0x30}, // Tab
|
||||
{0x2C, 0x69}, // Print Screen(F13)
|
||||
{0x2D, 0x72}, // Insert
|
||||
{0x2E, 0x75}, // Delete
|
||||
{0x24, 0x73}, // Home
|
||||
{0x23, 0x77}, // End
|
||||
{0x21, 0x74}, // Page Up
|
||||
{0x22, 0x79}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x25, 0x7B}, // Left Arrow
|
||||
{0x27, 0x7C}, // Right Arrow
|
||||
{0x26, 0x7E}, // Up Arrow
|
||||
{0x28, 0x7D}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x60, 0x52}, // Numpad 0
|
||||
{0x61, 0x53}, // Numpad 1
|
||||
{0x62, 0x54}, // Numpad 2
|
||||
{0x63, 0x55}, // Numpad 3
|
||||
{0x64, 0x56}, // Numpad 4
|
||||
{0x65, 0x57}, // Numpad 5
|
||||
{0x66, 0x58}, // Numpad 6
|
||||
{0x67, 0x59}, // Numpad 7
|
||||
{0x68, 0x5B}, // Numpad 8
|
||||
{0x69, 0x5C}, // Numpad 9
|
||||
{0x90, 0x47}, // Num Lock / Keypad Clear
|
||||
{0x6E, 0x41}, // Numpad .
|
||||
{0x6F, 0x4B}, // Numpad /
|
||||
{0x6A, 0x43}, // Numpad *
|
||||
{0x6D, 0x4E}, // Numpad -
|
||||
{0x6B, 0x45}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0xBA, 0x29}, // ; (Semicolon)
|
||||
{0xDE, 0x27}, // ' (Quote)
|
||||
{0xC0, 0x32}, // ` (Backtick)
|
||||
{0xBC, 0x2B}, // , (Comma)
|
||||
{0xBE, 0x2F}, // . (Period)
|
||||
{0xBF, 0x2C}, // / (Slash)
|
||||
{0xDC, 0x2A}, // \ (Backslash)
|
||||
{0xDB, 0x21}, // [ (Left Bracket)
|
||||
{0xDD, 0x1E}, // ] (Right Bracket)
|
||||
{0xBD, 0x1B}, // - (Minus)
|
||||
{0xBB, 0x18}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x14, 0x39}, // Caps Lock
|
||||
{0xA0, 0x38}, // Shift (Left)
|
||||
{0xA1, 0x3C}, // Shift (Right)
|
||||
{0xA2, 0x3B}, // Ctrl (Left)
|
||||
{0xA3, 0x3E}, // Ctrl (Right)
|
||||
{0xA4, 0x3A}, // Alt (Left)
|
||||
{0xA5, 0x3D}, // Alt (Right)
|
||||
{0x5B, 0x37}, // Left Command (Windows key)
|
||||
{0x5C, 0x36}, // Right Command
|
||||
};
|
||||
|
||||
// macOS CGKeyCode to Windows vkCode
|
||||
std::map<int, int> CGKeyCodeToVkCode = {
|
||||
// A-Z
|
||||
{0x00, 0x41}, // A
|
||||
{0x0B, 0x42}, // B
|
||||
{0x08, 0x43}, // C
|
||||
{0x02, 0x44}, // D
|
||||
{0x0E, 0x45}, // E
|
||||
{0x03, 0x46}, // F
|
||||
{0x05, 0x47}, // G
|
||||
{0x04, 0x48}, // H
|
||||
{0x22, 0x49}, // I
|
||||
{0x26, 0x4A}, // J
|
||||
{0x28, 0x4B}, // K
|
||||
{0x25, 0x4C}, // L
|
||||
{0x2E, 0x4D}, // M
|
||||
{0x2D, 0x4E}, // N
|
||||
{0x1F, 0x4F}, // O
|
||||
{0x23, 0x50}, // P
|
||||
{0x0C, 0x51}, // Q
|
||||
{0x0F, 0x52}, // R
|
||||
{0x01, 0x53}, // S
|
||||
{0x11, 0x54}, // T
|
||||
{0x20, 0x55}, // U
|
||||
{0x09, 0x56}, // V
|
||||
{0x0D, 0x57}, // W
|
||||
{0x07, 0x58}, // X
|
||||
{0x10, 0x59}, // Y
|
||||
{0x06, 0x5A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x1D, 0x30}, // 0
|
||||
{0x12, 0x31}, // 1
|
||||
{0x13, 0x32}, // 2
|
||||
{0x14, 0x33}, // 3
|
||||
{0x15, 0x34}, // 4
|
||||
{0x17, 0x35}, // 5
|
||||
{0x16, 0x36}, // 6
|
||||
{0x1A, 0x37}, // 7
|
||||
{0x1C, 0x38}, // 8
|
||||
{0x19, 0x39}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x7A, 0x70}, // F1
|
||||
{0x78, 0x71}, // F2
|
||||
{0x63, 0x72}, // F3
|
||||
{0x76, 0x73}, // F4
|
||||
{0x60, 0x74}, // F5
|
||||
{0x61, 0x75}, // F6
|
||||
{0x62, 0x76}, // F7
|
||||
{0x64, 0x77}, // F8
|
||||
{0x65, 0x78}, // F9
|
||||
{0x6D, 0x79}, // F10
|
||||
{0x67, 0x7A}, // F11
|
||||
{0x6F, 0x7B}, // F12
|
||||
|
||||
// control keys
|
||||
{0x35, 0x1B}, // Escape
|
||||
{0x24, 0x0D}, // Enter
|
||||
{0x31, 0x20}, // Space
|
||||
{0x33, 0x08}, // Backspace
|
||||
{0x30, 0x09}, // Tab
|
||||
{0x69, 0x2C}, // Print Screen(F13)
|
||||
{0x72, 0x2D}, // Insert
|
||||
{0x75, 0x2E}, // Delete
|
||||
{0x73, 0x24}, // Home
|
||||
{0x77, 0x23}, // End
|
||||
{0x74, 0x21}, // Page Up
|
||||
{0x79, 0x22}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x7B, 0x25}, // Left Arrow
|
||||
{0x7C, 0x27}, // Right Arrow
|
||||
{0x7E, 0x26}, // Up Arrow
|
||||
{0x7D, 0x28}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x52, 0x60}, // Numpad 0
|
||||
{0x53, 0x61}, // Numpad 1
|
||||
{0x54, 0x62}, // Numpad 2
|
||||
{0x55, 0x63}, // Numpad 3
|
||||
{0x56, 0x64}, // Numpad 4
|
||||
{0x57, 0x65}, // Numpad 5
|
||||
{0x58, 0x66}, // Numpad 6
|
||||
{0x59, 0x67}, // Numpad 7
|
||||
{0x5B, 0x68}, // Numpad 8
|
||||
{0x5C, 0x69}, // Numpad 9
|
||||
{0x47, 0x90}, // Num Lock / Keypad Clear
|
||||
{0x41, 0x6E}, // Numpad .
|
||||
{0x4B, 0x6F}, // Numpad /
|
||||
{0x43, 0x6A}, // Numpad *
|
||||
{0x4E, 0x6D}, // Numpad -
|
||||
{0x45, 0x6B}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x29, 0xBA}, // ; (Semicolon)
|
||||
{0x27, 0xDE}, // ' (Quote)
|
||||
{0x32, 0xC0}, // ` (Backtick)
|
||||
{0x2B, 0xBC}, // , (Comma)
|
||||
{0x2F, 0xBE}, // . (Period)
|
||||
{0x2C, 0xBF}, // / (Slash)
|
||||
{0x2A, 0xDC}, // \ (Backslash)
|
||||
{0x21, 0xDB}, // [ (Left Bracket)
|
||||
{0x1E, 0xDD}, // ] (Right Bracket)
|
||||
{0x1B, 0xBD}, // - (Minus)
|
||||
{0x18, 0xBB}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x39, 0x14}, // Caps Lock
|
||||
{0x38, 0xA0}, // Shift (Left)
|
||||
{0x3C, 0xA1}, // Shift (Right)
|
||||
{0x3B, 0xA2}, // Control (Left)
|
||||
{0x3E, 0xA3}, // Control (Right)
|
||||
{0x3A, 0xA4}, // Alt (Left)
|
||||
{0x3D, 0xA5}, // Alt (Right)
|
||||
{0x37, 0x5B}, // Left Command (Windows key)
|
||||
{0x36, 0x5C}, // Right Command
|
||||
};
|
||||
|
||||
// Windows vkCode to X11 KeySym
|
||||
std::map<int, int> vkCodeToX11KeySym = {
|
||||
// A-Z
|
||||
{0x41, 0x0041}, // A
|
||||
{0x42, 0x0042}, // B
|
||||
{0x43, 0x0043}, // C
|
||||
{0x44, 0x0044}, // D
|
||||
{0x45, 0x0045}, // E
|
||||
{0x46, 0x0046}, // F
|
||||
{0x47, 0x0047}, // G
|
||||
{0x48, 0x0048}, // H
|
||||
{0x49, 0x0049}, // I
|
||||
{0x4A, 0x004A}, // J
|
||||
{0x4B, 0x004B}, // K
|
||||
{0x4C, 0x004C}, // L
|
||||
{0x4D, 0x004D}, // M
|
||||
{0x4E, 0x004E}, // N
|
||||
{0x4F, 0x004F}, // O
|
||||
{0x50, 0x0050}, // P
|
||||
{0x51, 0x0051}, // Q
|
||||
{0x52, 0x0052}, // R
|
||||
{0x53, 0x0053}, // S
|
||||
{0x54, 0x0054}, // T
|
||||
{0x55, 0x0055}, // U
|
||||
{0x56, 0x0056}, // V
|
||||
{0x57, 0x0057}, // W
|
||||
{0x58, 0x0058}, // X
|
||||
{0x59, 0x0059}, // Y
|
||||
{0x5A, 0x005A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x30, 0x0030}, // 0
|
||||
{0x31, 0x0031}, // 1
|
||||
{0x32, 0x0032}, // 2
|
||||
{0x33, 0x0033}, // 3
|
||||
{0x34, 0x0034}, // 4
|
||||
{0x35, 0x0035}, // 5
|
||||
{0x36, 0x0036}, // 6
|
||||
{0x37, 0x0037}, // 7
|
||||
{0x38, 0x0038}, // 8
|
||||
{0x39, 0x0039}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x70, 0xFFBE}, // F1
|
||||
{0x71, 0xFFBF}, // F2
|
||||
{0x72, 0xFFC0}, // F3
|
||||
{0x73, 0xFFC1}, // F4
|
||||
{0x74, 0xFFC2}, // F5
|
||||
{0x75, 0xFFC3}, // F6
|
||||
{0x76, 0xFFC4}, // F7
|
||||
{0x77, 0xFFC5}, // F8
|
||||
{0x78, 0xFFC6}, // F9
|
||||
{0x79, 0xFFC7}, // F10
|
||||
{0x7A, 0xFFC8}, // F11
|
||||
{0x7B, 0xFFC9}, // F12
|
||||
|
||||
// control keys
|
||||
{0x1B, 0xFF1B}, // Escape
|
||||
{0x0D, 0xFF0D}, // Enter
|
||||
{0x20, 0x0020}, // Space
|
||||
{0x08, 0xFF08}, // Backspace
|
||||
{0x09, 0xFF09}, // Tab
|
||||
{0x2C, 0xFF15}, // Print Screen
|
||||
{0x91, 0xFF14}, // Scroll Lock
|
||||
{0x13, 0xFF13}, // Pause/Break
|
||||
{0x2D, 0xFF63}, // Insert
|
||||
{0x2E, 0xFFFF}, // Delete
|
||||
{0x24, 0xFF50}, // Home
|
||||
{0x23, 0xFF57}, // End
|
||||
{0x21, 0xFF55}, // Page Up
|
||||
{0x22, 0xFF56}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x25, 0xFF51}, // Left Arrow
|
||||
{0x27, 0xFF53}, // Right Arrow
|
||||
{0x26, 0xFF52}, // Up Arrow
|
||||
{0x28, 0xFF54}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x60, 0xFFB0}, // Numpad 0
|
||||
{0x61, 0xFFB1}, // Numpad 1
|
||||
{0x62, 0xFFB2}, // Numpad 2
|
||||
{0x63, 0xFFB3}, // Numpad 3
|
||||
{0x64, 0xFFB4}, // Numpad 4
|
||||
{0x65, 0xFFB5}, // Numpad 5
|
||||
{0x66, 0xFFB6}, // Numpad 6
|
||||
{0x67, 0xFFB7}, // Numpad 7
|
||||
{0x68, 0xFFB8}, // Numpad 8
|
||||
{0x69, 0xFFB9}, // Numpad 9
|
||||
{0x90, 0xFF7F}, // Num Lock
|
||||
{0x6E, 0xFFAE}, // Numpad .
|
||||
{0x6F, 0xFFAF}, // Numpad /
|
||||
{0x6A, 0xFFAA}, // Numpad *
|
||||
{0x6D, 0xFFAD}, // Numpad -
|
||||
{0x6B, 0xFFAB}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0xBA, 0x003B}, // ; (Semicolon)
|
||||
{0xDE, 0x0027}, // ' (Quote)
|
||||
{0xC0, 0x007E}, // ` (Grave)
|
||||
{0xBC, 0x002C}, // , (Comma)
|
||||
{0xBE, 0x002E}, // . (Period)
|
||||
{0xBF, 0x002F}, // / (Slash)
|
||||
{0xDC, 0x005C}, // \ (Backslash)
|
||||
{0xDB, 0x005B}, // [ (Left Bracket)
|
||||
{0xDD, 0x005D}, // ] (Right Bracket)
|
||||
{0xBD, 0x002D}, // - (Minus)
|
||||
{0xBB, 0x003D}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x14, 0xFFE5}, // Caps Lock
|
||||
{0xA0, 0xFFE1}, // Shift (Left)
|
||||
{0xA1, 0xFFE2}, // Shift (Right)
|
||||
{0xA2, 0xFFE3}, // Ctrl (Left)
|
||||
{0xA3, 0xFFE4}, // Ctrl (Right)
|
||||
{0xA4, 0xFFE9}, // Alt (Left)
|
||||
{0xA5, 0xFFEA}, // Alt (Right)
|
||||
{0x5B, 0xFFEB}, // Left Command (Windows key)
|
||||
{0x5C, 0xFFEC}, // Right Command
|
||||
};
|
||||
|
||||
// X11 KeySym to Windows vkCode
|
||||
// std::map<int, int> x11KeySymToVkCode = []() {
|
||||
// std::map<int, int> result;
|
||||
// for (const auto& pair : vkCodeToX11KeySym) {
|
||||
// result[pair.second] = pair.first;
|
||||
// }
|
||||
// return result;
|
||||
// }();
|
||||
|
||||
std::map<int, int> x11KeySymToVkCode = {
|
||||
// A-Z
|
||||
{0x0041, 0x41}, // A
|
||||
{0x0042, 0x42}, // B
|
||||
{0x0043, 0x43}, // C
|
||||
{0x0044, 0x44}, // D
|
||||
{0x0045, 0x45}, // E
|
||||
{0x0046, 0x46}, // F
|
||||
{0x0047, 0x47}, // G
|
||||
{0x0048, 0x48}, // H
|
||||
{0x0049, 0x49}, // I
|
||||
{0x004A, 0x4A}, // J
|
||||
{0x004B, 0x4B}, // K
|
||||
{0x004C, 0x4C}, // L
|
||||
{0x004D, 0x4D}, // M
|
||||
{0x004E, 0x4E}, // N
|
||||
{0x004F, 0x4F}, // O
|
||||
{0x0050, 0x50}, // P
|
||||
{0x0051, 0x51}, // Q
|
||||
{0x0052, 0x52}, // R
|
||||
{0x0053, 0x53}, // S
|
||||
{0x0054, 0x54}, // T
|
||||
{0x0055, 0x55}, // U
|
||||
{0x0056, 0x56}, // V
|
||||
{0x0057, 0x57}, // W
|
||||
{0x0058, 0x58}, // X
|
||||
{0x0059, 0x59}, // Y
|
||||
{0x005A, 0x5A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x0030, 0x30}, // 0
|
||||
{0x0031, 0x31}, // 1
|
||||
{0x0032, 0x32}, // 2
|
||||
{0x0033, 0x33}, // 3
|
||||
{0x0034, 0x34}, // 4
|
||||
{0x0035, 0x35}, // 5
|
||||
{0x0036, 0x36}, // 6
|
||||
{0x0037, 0x37}, // 7
|
||||
{0x0038, 0x38}, // 8
|
||||
{0x0039, 0x39}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0xFFBE, 0x70}, // F1
|
||||
{0xFFBF, 0x71}, // F2
|
||||
{0xFFC0, 0x72}, // F3
|
||||
{0xFFC1, 0x73}, // F4
|
||||
{0xFFC2, 0x74}, // F5
|
||||
{0xFFC3, 0x75}, // F6
|
||||
{0xFFC4, 0x76}, // F7
|
||||
{0xFFC5, 0x77}, // F8
|
||||
{0xFFC6, 0x78}, // F9
|
||||
{0xFFC7, 0x79}, // F10
|
||||
{0xFFC8, 0x7A}, // F11
|
||||
{0xFFC9, 0x7B}, // F12
|
||||
|
||||
// control keys
|
||||
{0xFF1B, 0x1B}, // Escape
|
||||
{0xFF0D, 0x0D}, // Enter
|
||||
{0x0020, 0x20}, // Space
|
||||
{0xFF08, 0x08}, // Backspace
|
||||
{0xFF09, 0x09}, // Tab
|
||||
{0xFF15, 0x2C}, // Print Screen
|
||||
{0xFF14, 0x91}, // Scroll Lock
|
||||
{0xFF13, 0x13}, // Pause/Break
|
||||
{0xFF63, 0x2D}, // Insert
|
||||
{0xFFFF, 0x2E}, // Delete
|
||||
{0xFF50, 0x24}, // Home
|
||||
{0xFF57, 0x23}, // End
|
||||
{0xFF55, 0x21}, // Page Up
|
||||
{0xFF56, 0x22}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0xFF51, 0x25}, // Left Arrow
|
||||
{0xFF53, 0x27}, // Right Arrow
|
||||
{0xFF52, 0x26}, // Up Arrow
|
||||
{0xFF54, 0x28}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0xFFB0, 0x60}, // Numpad 0
|
||||
{0xFFB1, 0x61}, // Numpad 1
|
||||
{0xFFB2, 0x62}, // Numpad 2
|
||||
{0xFFB3, 0x63}, // Numpad 3
|
||||
{0xFFB4, 0x64}, // Numpad 4
|
||||
{0xFFB5, 0x65}, // Numpad 5
|
||||
{0xFFB6, 0x66}, // Numpad 6
|
||||
{0xFFB7, 0x67}, // Numpad 7
|
||||
{0xFFB8, 0x68}, // Numpad 8
|
||||
{0xFFB9, 0x69}, // Numpad 9
|
||||
{0xFF7F, 0x90}, // Num Lock
|
||||
{0xFFAE, 0x6E}, // Numpad .
|
||||
{0xFFAF, 0x6F}, // Numpad /
|
||||
{0xFFAA, 0x6A}, // Numpad *
|
||||
{0xFFAD, 0x6D}, // Numpad -
|
||||
{0xFFAB, 0x6B}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x003B, 0xBA}, // ; (Semicolon)
|
||||
{0x0027, 0xDE}, // ' (Quote)
|
||||
{0x007E, 0xC0}, // ` (Grave)
|
||||
{0x002C, 0xBC}, // , (Comma)
|
||||
{0x002E, 0xBE}, // . (Period)
|
||||
{0x002F, 0xBF}, // / (Slash)
|
||||
{0x005C, 0xDC}, // \ (Backslash)
|
||||
{0x005B, 0xDB}, // [ (Left Bracket)
|
||||
{0x005D, 0xDD}, // ] (Right Bracket)
|
||||
{0x002D, 0xBD}, // - (Minus)
|
||||
{0x003D, 0xBB}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0xFFE5, 0x14}, // Caps Lock
|
||||
{0xFFE1, 0xA0}, // Shift (Left)
|
||||
{0xFFE2, 0xA1}, // Shift (Right)
|
||||
{0xFFE3, 0xA2}, // Ctrl (Left)
|
||||
{0xFFE4, 0xA3}, // Ctrl (Right)
|
||||
{0xFFE9, 0xA4}, // Alt (Left)
|
||||
{0xFFEA, 0xA5}, // Alt (Right)
|
||||
{0xFFEB, 0x5B}, // Left Command (Windows key)
|
||||
{0xFFEC, 0x5C}, // Right Command
|
||||
};
|
||||
|
||||
// macOS CGKeyCode to X11 KeySym
|
||||
std::map<int, int> cgKeyCodeToX11KeySym = {
|
||||
// A-Z
|
||||
{0x00, 0x0041}, // A
|
||||
{0x0B, 0x0042}, // B
|
||||
{0x08, 0x0043}, // C
|
||||
{0x02, 0x0044}, // D
|
||||
{0x0E, 0x0045}, // E
|
||||
{0x03, 0x0046}, // F
|
||||
{0x05, 0x0047}, // G
|
||||
{0x04, 0x0048}, // H
|
||||
{0x22, 0x0049}, // I
|
||||
{0x26, 0x004A}, // J
|
||||
{0x28, 0x004B}, // K
|
||||
{0x25, 0x004C}, // L
|
||||
{0x2E, 0x004D}, // M
|
||||
{0x2D, 0x004E}, // N
|
||||
{0x1F, 0x004F}, // O
|
||||
{0x23, 0x0050}, // P
|
||||
{0x0C, 0x0051}, // Q
|
||||
{0x0F, 0x0052}, // R
|
||||
{0x01, 0x0053}, // S
|
||||
{0x11, 0x0054}, // T
|
||||
{0x20, 0x0055}, // U
|
||||
{0x09, 0x0056}, // V
|
||||
{0x0D, 0x0057}, // W
|
||||
{0x07, 0x0058}, // X
|
||||
{0x10, 0x0059}, // Y
|
||||
{0x06, 0x005A}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x1D, 0x0030}, // 0
|
||||
{0x12, 0x0031}, // 1
|
||||
{0x13, 0x0032}, // 2
|
||||
{0x14, 0x0033}, // 3
|
||||
{0x15, 0x0034}, // 4
|
||||
{0x17, 0x0035}, // 5
|
||||
{0x16, 0x0036}, // 6
|
||||
{0x1A, 0x0037}, // 7
|
||||
{0x1C, 0x0038}, // 8
|
||||
{0x19, 0x0039}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0x7A, 0xFFBE}, // F1
|
||||
{0x78, 0xFFBF}, // F2
|
||||
{0x63, 0xFFC0}, // F3
|
||||
{0x76, 0xFFC1}, // F4
|
||||
{0x60, 0xFFC2}, // F5
|
||||
{0x61, 0xFFC3}, // F6
|
||||
{0x62, 0xFFC4}, // F7
|
||||
{0x64, 0xFFC5}, // F8
|
||||
{0x65, 0xFFC6}, // F9
|
||||
{0x6D, 0xFFC7}, // F10
|
||||
{0x67, 0xFFC8}, // F11
|
||||
{0x6F, 0xFFC9}, // F12
|
||||
|
||||
// control keys
|
||||
{0x35, 0xFF1B}, // Escape
|
||||
{0x24, 0xFF0D}, // Enter
|
||||
{0x31, 0x0020}, // Space
|
||||
{0x33, 0xFF08}, // Backspace
|
||||
{0x30, 0xFF09}, // Tab
|
||||
{0x69, 0xFF15}, // Print Screen(F13)
|
||||
{0x72, 0xFF63}, // Insert
|
||||
{0x75, 0xFFFF}, // Delete
|
||||
{0x73, 0xFF50}, // Home
|
||||
{0x77, 0xFF57}, // End
|
||||
{0x74, 0xFF55}, // Page Up
|
||||
{0x79, 0xFF56}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0x7B, 0xFF51}, // Left Arrow
|
||||
{0x7C, 0xFF53}, // Right Arrow
|
||||
{0x7E, 0xFF52}, // Up Arrow
|
||||
{0x7D, 0xFF54}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0x52, 0xFFB0}, // Numpad 0
|
||||
{0x53, 0xFFB1}, // Numpad 1
|
||||
{0x54, 0xFFB2}, // Numpad 2
|
||||
{0x55, 0xFFB3}, // Numpad 3
|
||||
{0x56, 0xFFB4}, // Numpad 4
|
||||
{0x57, 0xFFB5}, // Numpad 5
|
||||
{0x58, 0xFFB6}, // Numpad 6
|
||||
{0x59, 0xFFB7}, // Numpad 7
|
||||
{0x5B, 0xFFB8}, // Numpad 8
|
||||
{0x5C, 0xFFB9}, // Numpad 9
|
||||
{0x47, 0xFF7F}, // Num Lock / Keypad Clear
|
||||
{0x41, 0xFFAE}, // Numpad .
|
||||
{0x4B, 0xFFAF}, // Numpad /
|
||||
{0x43, 0xFFAA}, // Numpad *
|
||||
{0x4E, 0xFFAD}, // Numpad -
|
||||
{0x45, 0xFFAB}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x29, 0x003B}, // ; (Semicolon)
|
||||
{0x27, 0x0027}, // ' (Quote)
|
||||
{0x32, 0x007E}, // ` (Backtick)
|
||||
{0x2B, 0x002C}, // , (Comma)
|
||||
{0x2F, 0x002E}, // . (Period)
|
||||
{0x2C, 0x002F}, // / (Slash)
|
||||
{0x2A, 0x005C}, // \ (Backslash)
|
||||
{0x21, 0x005B}, // [ (Left Bracket)
|
||||
{0x1E, 0x005D}, // ] (Right Bracket)
|
||||
{0x1B, 0x002D}, // - (Minus)
|
||||
{0x18, 0x003D}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0x39, 0xFFE5}, // Caps Lock
|
||||
{0x38, 0xFFE1}, // Shift (Left)
|
||||
{0x3C, 0xFFE2}, // Shift (Right)
|
||||
{0x3B, 0xFFE3}, // Control (Left)
|
||||
{0x3E, 0xFFE4}, // Control (Right)
|
||||
{0x3A, 0xFFE9}, // Alt (Left)
|
||||
{0x3D, 0xFFEA}, // Alt (Right)
|
||||
{0x37, 0xFFEB}, // Left Command (Windows key)
|
||||
{0x36, 0xFFEC}, // Right Command
|
||||
};
|
||||
|
||||
// X11 KeySym to macOS CGKeyCode
|
||||
// std::map<int, int> x11KeySymToCgKeyCode = []() {
|
||||
// std::map<int, int> result;
|
||||
// for (const auto& pair : cgKeyCodeToX11KeySym) {
|
||||
// result[pair.second] = pair.first;
|
||||
// }
|
||||
// return result;
|
||||
// }();
|
||||
|
||||
std::map<int, int> x11KeySymToCgKeyCode = {
|
||||
// A-Z
|
||||
{0x0041, 0x00}, // A
|
||||
{0x0042, 0x0B}, // B
|
||||
{0x0043, 0x08}, // C
|
||||
{0x0044, 0x02}, // D
|
||||
{0x0045, 0x0E}, // E
|
||||
{0x0046, 0x03}, // F
|
||||
{0x0047, 0x05}, // G
|
||||
{0x0048, 0x04}, // H
|
||||
{0x0049, 0x22}, // I
|
||||
{0x004A, 0x26}, // J
|
||||
{0x004B, 0x28}, // K
|
||||
{0x004C, 0x25}, // L
|
||||
{0x004D, 0x2E}, // M
|
||||
{0x004E, 0x2D}, // N
|
||||
{0x004F, 0x1F}, // O
|
||||
{0x0050, 0x23}, // P
|
||||
{0x0051, 0x0C}, // Q
|
||||
{0x0052, 0x0F}, // R
|
||||
{0x0053, 0x01}, // S
|
||||
{0x0054, 0x11}, // T
|
||||
{0x0055, 0x20}, // U
|
||||
{0x0056, 0x09}, // V
|
||||
{0x0057, 0x0D}, // W
|
||||
{0x0058, 0x07}, // X
|
||||
{0x0059, 0x10}, // Y
|
||||
{0x005A, 0x06}, // Z
|
||||
|
||||
// 0-9
|
||||
{0x0030, 0x1D}, // 0
|
||||
{0x0031, 0x12}, // 1
|
||||
{0x0032, 0x13}, // 2
|
||||
{0x0033, 0x14}, // 3
|
||||
{0x0034, 0x15}, // 4
|
||||
{0x0035, 0x17}, // 5
|
||||
{0x0036, 0x16}, // 6
|
||||
{0x0037, 0x1A}, // 7
|
||||
{0x0038, 0x1C}, // 8
|
||||
{0x0039, 0x19}, // 9
|
||||
|
||||
// F1-F12
|
||||
{0xFFBE, 0x7A}, // F1
|
||||
{0xFFBF, 0x78}, // F2
|
||||
{0xFFC0, 0x63}, // F3
|
||||
{0xFFC1, 0x76}, // F4
|
||||
{0xFFC2, 0x60}, // F5
|
||||
{0xFFC3, 0x61}, // F6
|
||||
{0xFFC4, 0x62}, // F7
|
||||
{0xFFC5, 0x64}, // F8
|
||||
{0xFFC6, 0x65}, // F9
|
||||
{0xFFC7, 0x6D}, // F10
|
||||
{0xFFC8, 0x67}, // F11
|
||||
{0xFFC9, 0x6F}, // F12
|
||||
|
||||
// control keys
|
||||
{0xFF1B, 0x35}, // Escape
|
||||
{0xFF0D, 0x24}, // Enter
|
||||
{0x0020, 0x31}, // Space
|
||||
{0xFF08, 0x33}, // Backspace
|
||||
{0xFF09, 0x30}, // Tab
|
||||
{0xFF15, 0x69}, // Print Screen(F13)
|
||||
{0xFF63, 0x72}, // Insert
|
||||
{0xFFFF, 0x75}, // Delete
|
||||
{0xFF50, 0x73}, // Home
|
||||
{0xFF57, 0x77}, // End
|
||||
{0xFF55, 0x74}, // Page Up
|
||||
{0xFF56, 0x79}, // Page Down
|
||||
|
||||
// arrow keys
|
||||
{0xFF51, 0x7B}, // Left Arrow
|
||||
{0xFF53, 0x7C}, // Right Arrow
|
||||
{0xFF52, 0x7E}, // Up Arrow
|
||||
{0xFF54, 0x7D}, // Down Arrow
|
||||
|
||||
// numpad
|
||||
{0xFFB0, 0x52}, // Numpad 0
|
||||
{0xFFB1, 0x53}, // Numpad 1
|
||||
{0xFFB2, 0x54}, // Numpad 2
|
||||
{0xFFB3, 0x55}, // Numpad 3
|
||||
{0xFFB4, 0x56}, // Numpad 4
|
||||
{0xFFB5, 0x57}, // Numpad 5
|
||||
{0xFFB6, 0x58}, // Numpad 6
|
||||
{0xFFB7, 0x59}, // Numpad 7
|
||||
{0xFFB8, 0x5B}, // Numpad 8
|
||||
{0xFFB9, 0x5C}, // Numpad 9
|
||||
{0xFF7F, 0x47}, // Num Lock / Keypad Clear
|
||||
{0xFFAE, 0x41}, // Numpad .
|
||||
{0xFFAF, 0x4B}, // Numpad /
|
||||
{0xFFAA, 0x43}, // Numpad *
|
||||
{0xFFAD, 0x4E}, // Numpad -
|
||||
{0xFFAB, 0x45}, // Numpad +
|
||||
|
||||
// symbol keys
|
||||
{0x003B, 0x29}, // ; (Semicolon)
|
||||
{0x0027, 0x27}, // ' (Quote)
|
||||
{0x007E, 0x32}, // ` (Backtick)
|
||||
{0x002C, 0x2B}, // , (Comma)
|
||||
{0x002E, 0x2F}, // . (Period)
|
||||
{0x002F, 0x2C}, // / (Slash)
|
||||
{0x005C, 0x2A}, // \ (Backslash)
|
||||
{0x005B, 0x21}, // [ (Left Bracket)
|
||||
{0x005D, 0x1E}, // ] (Right Bracket)
|
||||
{0x002D, 0x1B}, // - (Minus)
|
||||
{0x003D, 0x18}, // = (Equals)
|
||||
|
||||
// modifier keys
|
||||
{0xFFE5, 0x39}, // Caps Lock
|
||||
{0xFFE1, 0x38}, // Shift (Left)
|
||||
{0xFFE2, 0x3C}, // Shift (Right)
|
||||
{0xFFE3, 0x3B}, // Control (Left)
|
||||
{0xFFE4, 0x3E}, // Control (Right)
|
||||
{0xFFE9, 0x3A}, // Alt (Left)
|
||||
{0xFFEA, 0x3D}, // Alt (Right)
|
||||
{0xFFEB, 0x37}, // Left Command
|
||||
{0xFFEC, 0x36}, // Right Command
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -0,0 +1,195 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16;
|
||||
|
||||
bool main_window_minimized_ = 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;
|
||||
};
|
||||
|
||||
// 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_window_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_;
|
||||
};
|
||||
|
||||
struct UpdateState {
|
||||
nlohmann::json latest_version_info_ = nlohmann::json{};
|
||||
bool update_available_ = false;
|
||||
std::string latest_version_;
|
||||
std::string release_name_;
|
||||
std::string release_notes_;
|
||||
std::string release_date_;
|
||||
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 {
|
||||
bool need_to_create_stream_window_ = false;
|
||||
bool stream_window_created_ = false;
|
||||
bool stream_window_inited_ = false;
|
||||
bool window_maximized_ = 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;
|
||||
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 {
|
||||
// Transport callbacks run outside the UI thread. Keep the two lifecycle
|
||||
// requests atomic so a disconnect cannot be lost while the Slint timer is
|
||||
// deciding whether to create or destroy the controlled-side window.
|
||||
std::atomic<bool> need_to_create_server_window_{false};
|
||||
std::atomic<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;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
float server_window_dpi_scaling_h_ = 1.0f;
|
||||
float window_rounding_ = 6.0f;
|
||||
float window_rounding_default_ = 6.0f;
|
||||
bool server_window_collapsed_ = false;
|
||||
bool server_window_collapsed_dragging_ = false;
|
||||
float server_window_collapsed_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_collapsed_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_collapsed_drag_start_win_x_ = 0;
|
||||
int server_window_collapsed_drag_start_win_y_ = 0;
|
||||
bool server_window_dragging_ = false;
|
||||
float server_window_drag_start_mouse_x_ = 0.0f;
|
||||
float server_window_drag_start_mouse_y_ = 0.0f;
|
||||
int server_window_drag_start_win_x_ = 0;
|
||||
int server_window_drag_start_win_y_ = 0;
|
||||
};
|
||||
|
||||
struct UiState {
|
||||
bool label_inited_ = false;
|
||||
bool connect_button_pressed_ = false;
|
||||
bool password_validating_ = false;
|
||||
uint32_t password_validating_time_ = 0;
|
||||
bool show_settings_window_ = false;
|
||||
bool show_self_hosted_server_config_window_ = false;
|
||||
bool rejoin_ = false;
|
||||
bool local_id_copied_ = false;
|
||||
bool show_password_ = true;
|
||||
bool show_about_window_ = false;
|
||||
bool show_connection_status_window_ = false;
|
||||
bool show_reset_password_window_ = false;
|
||||
bool show_update_notification_window_ = false;
|
||||
bool fullscreen_button_pressed_ = false;
|
||||
bool focus_on_input_widget_ = true;
|
||||
bool is_client_mode_ = false;
|
||||
bool is_server_mode_ = false;
|
||||
bool reload_recent_connections_ = true;
|
||||
bool show_confirm_delete_connection_ = false;
|
||||
bool show_edit_connection_alias_window_ = false;
|
||||
bool show_offline_warning_window_ = false;
|
||||
bool delete_connection_ = false;
|
||||
bool is_tab_bar_hovered_ = false;
|
||||
std::string delete_connection_name_;
|
||||
std::string edit_connection_alias_remote_id_;
|
||||
char edit_connection_alias_[128] = "";
|
||||
std::string offline_warning_text_;
|
||||
bool re_enter_remote_id_ = false;
|
||||
double copy_start_time_ = 0;
|
||||
};
|
||||
|
||||
struct ApplicationState : MainWindowState,
|
||||
InteractionState,
|
||||
UpdateState,
|
||||
StreamWindowState,
|
||||
ServerWindowState,
|
||||
UiState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
@@ -0,0 +1,97 @@
|
||||
#ifndef _GUI_APPLICATION_H_
|
||||
#define _GUI_APPLICATION_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "runtime/cursor_state_provider.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Slint application shell. GuiRuntime remains the owner of transport, media,
|
||||
// device, clipboard, transfer and settings behavior.
|
||||
class GuiApplication final : private GuiRuntime {
|
||||
public:
|
||||
GuiApplication();
|
||||
~GuiApplication();
|
||||
|
||||
int Run();
|
||||
|
||||
private:
|
||||
struct SlintUi;
|
||||
struct CursorDeliveryState {
|
||||
CursorState last_sent{};
|
||||
bool has_sent = false;
|
||||
bool feedback_pending = false;
|
||||
std::chrono::steady_clock::time_point last_sent_time{};
|
||||
};
|
||||
|
||||
void InitializeLogger();
|
||||
void InitializeSettings();
|
||||
bool InitializeSDL();
|
||||
void InitializeModules();
|
||||
void InitializeUi();
|
||||
void InitializeSystemTray();
|
||||
bool MinimizeMainWindowToTray();
|
||||
void BindMainCallbacks();
|
||||
void BindStreamCallbacks();
|
||||
void BindServerCallbacks();
|
||||
void Tick();
|
||||
void ShareLocalCursorState();
|
||||
void HandlePasswordChangeResult();
|
||||
void HandleCredentialRecovery();
|
||||
void SyncMainWindow();
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
void SyncStreamVideoFrame();
|
||||
void ScheduleNextVideoFrame();
|
||||
void ConfigureStreamVideoRenderer();
|
||||
void SyncStreamKeyboardFocus();
|
||||
void SetStreamKeyboardFocus(bool focused);
|
||||
void SyncServerWindow();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
void SyncXWaylandWindowActivation();
|
||||
#endif
|
||||
void UpdateLocalization();
|
||||
void ResetSettingsUi();
|
||||
void SaveSettingsFromUi();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
void CheckPortableWindowsService();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
void ConnectFromUi(const std::string &remote_id);
|
||||
void SelectStreamTab(int index);
|
||||
void ReorderStreamTab(int from, float drop_x, float tab_width);
|
||||
void CloseStreamTab(const std::string &remote_id);
|
||||
void SendPointerInput(int button, int kind, float x, float y);
|
||||
void SendScrollInput(float delta_x, float delta_y, float x, float y);
|
||||
void SendKeyInput(const std::string &text, bool pressed, bool control,
|
||||
bool alt, bool shift, bool meta);
|
||||
void Cleanup();
|
||||
|
||||
std::shared_ptr<RemoteSession> SelectedSession();
|
||||
std::string OpenFileDialog(const std::string &title);
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
uint32_t cursor_state_sequence_ = 0;
|
||||
std::chrono::steady_clock::time_point next_video_frame_time_{};
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
bool use_xwayland_gui_ = false;
|
||||
bool use_x11_custom_titlebar_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,323 @@
|
||||
#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_)) {
|
||||
if (!MinimizeMainWindowToTray()) {
|
||||
SDL_HideWindow(main_window_);
|
||||
}
|
||||
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_window_events_ &&
|
||||
focus_on_stream_window_ && stream_window_ &&
|
||||
SDL_GetWindowID(stream_window_) == event.key.windowID) {
|
||||
ProcessKeyboardEvent(event);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (event.type == STREAM_REFRESH_EVENT) {
|
||||
auto *props = static_cast<RemoteSession *>(event.user.data1);
|
||||
if (!props) {
|
||||
break;
|
||||
}
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
bool render_rect_dirty = false;
|
||||
bool cleanup_pending = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
cleanup_pending = props->stream_cleanup_pending_;
|
||||
if (!cleanup_pending) {
|
||||
frame_snapshot = props->front_frame_;
|
||||
video_width = props->video_width_;
|
||||
video_height = props->video_height_;
|
||||
}
|
||||
render_rect_dirty = props->render_rect_dirty_;
|
||||
}
|
||||
|
||||
if (cleanup_pending) {
|
||||
if (props->stream_texture_) {
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (video_width <= 0 || video_height <= 0) {
|
||||
break;
|
||||
}
|
||||
if (!frame_snapshot || frame_snapshot->empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (props->stream_texture_) {
|
||||
if (video_width != props->texture_width_ ||
|
||||
video_height != props->texture_height_) {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
// props->texture_height_);
|
||||
|
||||
SDL_PropertiesID nvProps = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
props->texture_width_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
props->texture_height_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_NV12);
|
||||
SDL_SetNumberProperty(nvProps,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
SDL_COLORSPACE_BT601_LIMITED);
|
||||
props->stream_texture_ =
|
||||
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
} else {
|
||||
props->texture_width_ = video_width;
|
||||
props->texture_height_ = video_height;
|
||||
// props->stream_texture_ = SDL_CreateTexture(
|
||||
// stream_renderer_, stream_pixformat_,
|
||||
// SDL_TEXTUREACCESS_STREAMING, props->texture_width_,
|
||||
// props->texture_height_);
|
||||
|
||||
SDL_PropertiesID nvProps = SDL_CreateProperties();
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER,
|
||||
props->texture_width_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER,
|
||||
props->texture_height_);
|
||||
SDL_SetNumberProperty(nvProps, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER,
|
||||
SDL_PIXELFORMAT_NV12);
|
||||
SDL_SetNumberProperty(nvProps,
|
||||
SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER,
|
||||
SDL_COLORSPACE_BT601_LIMITED);
|
||||
props->stream_texture_ =
|
||||
SDL_CreateTextureWithProperties(stream_renderer_, nvProps);
|
||||
SDL_DestroyProperties(nvProps);
|
||||
}
|
||||
|
||||
SDL_UpdateTexture(props->stream_texture_, NULL, frame_snapshot->data(),
|
||||
props->texture_width_);
|
||||
|
||||
if (render_rect_dirty) {
|
||||
UpdateRenderRect();
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->render_rect_dirty_ = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,424 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <remote_action.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;
|
||||
}
|
||||
|
||||
// SDL represents key auto-repeat as additional key-down events. Forward
|
||||
// them so the fallback path has the same long-press behavior as native hooks.
|
||||
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{};
|
||||
|
||||
if ((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
|
||||
event.button.button != SDL_BUTTON_LEFT &&
|
||||
event.button.button != SDL_BUTTON_RIGHT &&
|
||||
event.button.button != SDL_BUTTON_MIDDLE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
float cursor_x = last_mouse_event.motion.x;
|
||||
float cursor_y = last_mouse_event.motion.y;
|
||||
|
||||
auto normalize_cursor_to_window_space = [&](float *x, float *y) {
|
||||
if (!x || !y || !stream_window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
int window_width = 0;
|
||||
int window_height = 0;
|
||||
int pixel_width = 0;
|
||||
int pixel_height = 0;
|
||||
SDL_GetWindowSize(stream_window_, &window_width, &window_height);
|
||||
SDL_GetWindowSizeInPixels(stream_window_, &pixel_width, &pixel_height);
|
||||
|
||||
if (window_width <= 0 || window_height <= 0 || pixel_width <= 0 ||
|
||||
pixel_height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((window_width != pixel_width || window_height != pixel_height) &&
|
||||
(*x > static_cast<float>(window_width) + 1.0f ||
|
||||
*y > static_cast<float>(window_height) + 1.0f)) {
|
||||
const float scale_x =
|
||||
static_cast<float>(window_width) / static_cast<float>(pixel_width);
|
||||
const float scale_y =
|
||||
static_cast<float>(window_height) / static_cast<float>(pixel_height);
|
||||
*x *= scale_x;
|
||||
*y *= scale_y;
|
||||
|
||||
static bool logged_pixel_to_window_conversion = false;
|
||||
if (!logged_pixel_to_window_conversion) {
|
||||
LOG_INFO(
|
||||
"Mouse coordinate space converted from pixels to window units: "
|
||||
"window={}x{}, pixels={}x{}, scale=({:.4f},{:.4f})",
|
||||
window_width, window_height, pixel_width, pixel_height, scale_x,
|
||||
scale_y);
|
||||
logged_pixel_to_window_conversion = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
cursor_x = event.motion.x;
|
||||
cursor_y = event.motion.y;
|
||||
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
|
||||
} else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) {
|
||||
cursor_x = event.button.x;
|
||||
cursor_y = event.button.y;
|
||||
normalize_cursor_to_window_space(&cursor_x, &cursor_y);
|
||||
} else if (event.type == SDL_EVENT_MOUSE_WHEEL) {
|
||||
cursor_x = last_mouse_event.motion.x;
|
||||
cursor_y = last_mouse_event.motion.y;
|
||||
}
|
||||
|
||||
const bool is_pointer_position_event =
|
||||
(event.type == SDL_EVENT_MOUSE_MOTION ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP);
|
||||
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (!props->control_mouse_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool file_transfer_window_hovered =
|
||||
props->file_transfer_.file_transfer_window_hovered_;
|
||||
const bool overlay_hovered =
|
||||
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
|
||||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
|
||||
|
||||
const SDL_FRect render_rect = props->stream_render_rect_f_;
|
||||
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_pointer_position_event && cursor_x >= render_rect.x &&
|
||||
cursor_x <= render_rect.x + render_rect.w &&
|
||||
cursor_y >= render_rect.y &&
|
||||
cursor_y <= render_rect.y + render_rect.h) {
|
||||
controlled_remote_id_ = it.first;
|
||||
last_mouse_event.motion.x = cursor_x;
|
||||
last_mouse_event.motion.y = cursor_y;
|
||||
last_mouse_event.button.x = cursor_x;
|
||||
last_mouse_event.button.y = cursor_y;
|
||||
|
||||
remote_action.m.x = (cursor_x - render_rect.x) / render_rect.w;
|
||||
remote_action.m.y = (cursor_y - render_rect.y) / render_rect.h;
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (SDL_EVENT_MOUSE_BUTTON_DOWN == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
if (SDL_BUTTON_LEFT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::left_down;
|
||||
} else if (SDL_BUTTON_RIGHT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::right_down;
|
||||
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::middle_down;
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_BUTTON_UP == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
if (SDL_BUTTON_LEFT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::left_up;
|
||||
} else if (SDL_BUTTON_RIGHT == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::right_up;
|
||||
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
|
||||
remote_action.m.flag = MouseFlag::middle_up;
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_MOTION == event.type) {
|
||||
remote_action.type = ControlType::mouse;
|
||||
remote_action.m.flag = MouseFlag::move;
|
||||
}
|
||||
|
||||
if (overlay_hovered) {
|
||||
break;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
} else if (SDL_EVENT_MOUSE_WHEEL == event.type &&
|
||||
last_mouse_event.button.x >= render_rect.x &&
|
||||
last_mouse_event.button.x <= render_rect.x + render_rect.w &&
|
||||
last_mouse_event.button.y >= render_rect.y &&
|
||||
last_mouse_event.button.y <= render_rect.y + render_rect.h) {
|
||||
float scroll_x = event.wheel.x;
|
||||
float scroll_y = event.wheel.y;
|
||||
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
|
||||
scroll_x = -scroll_x;
|
||||
scroll_y = -scroll_y;
|
||||
}
|
||||
|
||||
remote_action.type = ControlType::mouse;
|
||||
|
||||
auto roundUp = [](float value) -> int {
|
||||
if (value > 0) {
|
||||
return static_cast<int>(std::ceil(value));
|
||||
} else if (value < 0) {
|
||||
return static_cast<int>(std::floor(value));
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
|
||||
remote_action.m.flag = MouseFlag::wheel_vertical;
|
||||
remote_action.m.s = roundUp(scroll_y);
|
||||
} else {
|
||||
remote_action.m.flag = MouseFlag::wheel_horizontal;
|
||||
remote_action.m.s = roundUp(scroll_x);
|
||||
}
|
||||
|
||||
remote_action.m.x = (last_mouse_event.button.x - render_rect.x) /
|
||||
(std::max)(render_rect.w, 1.0f);
|
||||
remote_action.m.y = (last_mouse_event.button.y - render_rect.y) /
|
||||
(std::max)(render_rect.h, 1.0f);
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (overlay_hovered) {
|
||||
continue;
|
||||
}
|
||||
if (props->peer_) {
|
||||
std::string msg = remote_action.to_json();
|
||||
SendDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->mouse_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk::server_window_state {
|
||||
|
||||
inline int ReconcileSelectedController(const std::vector<std::string>& ids,
|
||||
std::string* selected_id) {
|
||||
if (!selected_id) {
|
||||
return 0;
|
||||
}
|
||||
if (ids.empty()) {
|
||||
selected_id->clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto selected = std::find(ids.begin(), ids.end(), *selected_id);
|
||||
if (selected == ids.end()) {
|
||||
*selected_id = ids.front();
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(std::distance(ids.begin(), selected));
|
||||
}
|
||||
|
||||
} // namespace crossdesk::server_window_state
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace crossdesk::window_geometry {
|
||||
|
||||
struct PhysicalRect {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct PhysicalSize {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
struct PhysicalPosition {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
};
|
||||
|
||||
constexpr PhysicalPosition CenteredPosition(const PhysicalRect& bounds,
|
||||
const PhysicalSize& size) {
|
||||
return {
|
||||
bounds.x + (bounds.width - std::min(bounds.width, size.width)) / 2,
|
||||
bounds.y + (bounds.height - std::min(bounds.height, size.height)) / 2,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr PhysicalPosition BottomRightPosition(const PhysicalRect& bounds,
|
||||
const PhysicalSize& size) {
|
||||
return {
|
||||
bounds.x + std::max(0, bounds.width - size.width),
|
||||
bounds.y + std::max(0, bounds.height - size.height),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace crossdesk::window_geometry
|
||||
@@ -0,0 +1,687 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <libyuv.h>
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xlib.h>
|
||||
#endif
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "fa_regular_400.h"
|
||||
#include "fa_solid_900.h"
|
||||
#include "layout_relative.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "version_checker.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "window_util_mac.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
const ImWchar *GetMultilingualGlyphRanges() {
|
||||
static std::vector<ImWchar> glyph_ranges;
|
||||
if (glyph_ranges.empty()) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImFontGlyphRangesBuilder builder;
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesChineseFull());
|
||||
builder.AddRanges(io.Fonts->GetGlyphRangesCyrillic());
|
||||
|
||||
ImVector<ImWchar> built_ranges;
|
||||
builder.BuildRanges(&built_ranges);
|
||||
glyph_ranges.assign(built_ranges.Data,
|
||||
built_ranges.Data + built_ranges.Size);
|
||||
}
|
||||
return glyph_ranges.empty() ? nullptr : glyph_ranges.data();
|
||||
}
|
||||
|
||||
bool CanReadFontFile(const char *font_path) {
|
||||
if (!font_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream font_file(font_path, std::ios::binary);
|
||||
return font_file.good();
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
HICON LoadTrayIcon() {
|
||||
HMODULE module = GetModuleHandleW(nullptr);
|
||||
HICON icon = reinterpret_cast<HICON>(
|
||||
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
|
||||
if (icon) {
|
||||
return icon;
|
||||
}
|
||||
|
||||
return LoadIconW(nullptr, IDI_APPLICATION);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
inline bool X11GetDisplayAndWindow(SDL_Window *window, Display **display_out,
|
||||
::Window *x11_window_out) {
|
||||
if (!window || !display_out || !x11_window_out) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(SDL_PROP_WINDOW_X11_DISPLAY_POINTER) || \
|
||||
!defined(SDL_PROP_WINDOW_X11_WINDOW_NUMBER)
|
||||
// SDL build does not expose X11 window properties.
|
||||
return false;
|
||||
#else
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
Display *display = (Display *)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
|
||||
const Sint64 x11_window_num =
|
||||
SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
|
||||
const ::Window x11_window = (::Window)x11_window_num;
|
||||
|
||||
if (!display || !x11_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*display_out = display;
|
||||
*x11_window_out = x11_window;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void X11SendNetWmState(Display *display, ::Window x11_window,
|
||||
long action, Atom state1, Atom state2 = 0) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_state = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
|
||||
XEvent event;
|
||||
memset(&event, 0, sizeof(event));
|
||||
event.xclient.type = ClientMessage;
|
||||
event.xclient.serial = 0;
|
||||
event.xclient.send_event = True;
|
||||
event.xclient.message_type = wm_state;
|
||||
event.xclient.window = x11_window;
|
||||
event.xclient.format = 32;
|
||||
event.xclient.data.l[0] = action;
|
||||
event.xclient.data.l[1] = (long)state1;
|
||||
event.xclient.data.l[2] = (long)state2;
|
||||
event.xclient.data.l[3] = 1; // normal source indication
|
||||
event.xclient.data.l[4] = 0;
|
||||
|
||||
XSendEvent(display, DefaultRootWindow(display), False,
|
||||
SubstructureRedirectMask | SubstructureNotifyMask, &event);
|
||||
}
|
||||
|
||||
inline void X11SetWindowTypeUtility(Display *display, ::Window x11_window) {
|
||||
if (!display || !x11_window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom wm_window_type =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom wm_window_type_utility =
|
||||
XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
|
||||
XChangeProperty(display, x11_window, wm_window_type, XA_ATOM, 32,
|
||||
PropModeReplace, (unsigned char *)&wm_window_type_utility, 1);
|
||||
}
|
||||
|
||||
inline void X11SetWindowAlwaysOnTop(SDL_Window *window) {
|
||||
Display *display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom state_above = XInternAtom(display, "_NET_WM_STATE_ABOVE", False);
|
||||
const Atom state_stays_on_top =
|
||||
XInternAtom(display, "_NET_WM_STATE_STAYS_ON_TOP", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for ABOVE + STAYS_ON_TOP.
|
||||
X11SendNetWmState(display, x11_window, 1, state_above, state_stays_on_top);
|
||||
XFlush(display);
|
||||
}
|
||||
|
||||
inline void X11SetWindowSkipTaskbar(SDL_Window *window) {
|
||||
Display *display = nullptr;
|
||||
::Window x11_window = 0;
|
||||
if (!X11GetDisplayAndWindow(window, &display, &x11_window)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Atom skip_taskbar =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_TASKBAR", False);
|
||||
const Atom skip_pager =
|
||||
XInternAtom(display, "_NET_WM_STATE_SKIP_PAGER", False);
|
||||
|
||||
// Request _NET_WM_STATE_ADD for SKIP_TASKBAR + SKIP_PAGER.
|
||||
X11SendNetWmState(display, x11_window, 1, skip_taskbar, skip_pager);
|
||||
|
||||
// Hint the WM that this is an auxiliary/utility window.
|
||||
X11SetWindowTypeUtility(display, x11_window);
|
||||
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
|
||||
SDL_HitTestResult GuiApplication::HitTestCallback(SDL_Window *window,
|
||||
const SDL_Point *area,
|
||||
void *data) {
|
||||
GuiApplication *application = (GuiApplication *)data;
|
||||
if (!application) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
if (application->fullscreen_button_pressed_) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
// Server window: OS-level dragging for the title bar, but keep the left-side
|
||||
// collapse/expand button clickable.
|
||||
if (application->server_window_ && window == application->server_window_) {
|
||||
const float title_h = application->server_window_title_bar_height_;
|
||||
const float button_w = title_h;
|
||||
if (area->y >= 0 && area->y < title_h) {
|
||||
if (area->x >= 0 && area->x < button_w) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int window_width, window_height;
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
|
||||
// check if curosor is in tab bar
|
||||
if (application->stream_window_inited_ && application->stream_window_created_ &&
|
||||
!application->fullscreen_button_pressed_ && application->stream_ctx_) {
|
||||
ImGuiContext *prev_ctx = ImGui::GetCurrentContext();
|
||||
ImGui::SetCurrentContext(application->stream_ctx_);
|
||||
|
||||
ImGuiWindow *tab_bar_window = ImGui::FindWindowByName("TabBar");
|
||||
if (tab_bar_window && tab_bar_window->Active) {
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float scale_x = io.DisplayFramebufferScale.x;
|
||||
float scale_y = io.DisplayFramebufferScale.y;
|
||||
|
||||
float tab_bar_x = tab_bar_window->Pos.x * scale_x;
|
||||
float tab_bar_y = tab_bar_window->Pos.y * scale_y;
|
||||
float tab_bar_width = tab_bar_window->Size.x * scale_x;
|
||||
float tab_bar_height = tab_bar_window->Size.y * scale_y;
|
||||
|
||||
ImGui::SetCurrentContext(prev_ctx);
|
||||
|
||||
if (area->x >= tab_bar_x && area->x <= tab_bar_x + tab_bar_width &&
|
||||
area->y >= tab_bar_y && area->y <= tab_bar_y + tab_bar_height) {
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
} else {
|
||||
ImGui::SetCurrentContext(prev_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
float mouse_grab_padding = application->title_bar_button_width_ * 0.16f;
|
||||
if (area->y < application->title_bar_button_width_ &&
|
||||
area->y > mouse_grab_padding &&
|
||||
area->x < window_width - application->title_bar_button_width_ * 3.0f &&
|
||||
area->x > mouse_grab_padding) {
|
||||
return SDL_HITTEST_DRAGGABLE;
|
||||
}
|
||||
|
||||
// if (!application->streaming_) {
|
||||
// return SDL_HITTEST_NORMAL;
|
||||
// }
|
||||
|
||||
if (area->y < mouse_grab_padding) {
|
||||
if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_TOPLEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_TOPRIGHT;
|
||||
} else {
|
||||
return SDL_HITTEST_RESIZE_TOP;
|
||||
}
|
||||
} else if (area->y > window_height - mouse_grab_padding) {
|
||||
if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_BOTTOMLEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_BOTTOMRIGHT;
|
||||
} else {
|
||||
return SDL_HITTEST_RESIZE_BOTTOM;
|
||||
}
|
||||
} else if (area->x < mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_LEFT;
|
||||
} else if (area->x > window_width - mouse_grab_padding) {
|
||||
return SDL_HITTEST_RESIZE_RIGHT;
|
||||
}
|
||||
|
||||
return SDL_HITTEST_NORMAL;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateMainWindow() {
|
||||
main_ctx_ = ImGui::CreateContext();
|
||||
if (!main_ctx_) {
|
||||
LOG_ERROR("Main context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Main Window", (int)main_window_width_,
|
||||
(int)main_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_HIDDEN | SDL_WINDOW_TRANSPARENT,
|
||||
&main_window_, &main_renderer_)) {
|
||||
LOG_ERROR("Error creating MainWindow and MainRenderer: {}", SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
float dpi_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
|
||||
if (std::abs(dpi_scale_ - dpi_scale) > 0.01f) {
|
||||
dpi_scale_ = dpi_scale;
|
||||
|
||||
main_window_width_ = (int)(main_window_width_default_ * dpi_scale_);
|
||||
main_window_height_ = (int)(main_window_height_default_ * dpi_scale_);
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
server_window_width_ = (int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_height_ = (int)(server_window_height_default_ * dpi_scale_);
|
||||
server_window_normal_width_ =
|
||||
(int)(server_window_width_default_ * dpi_scale_);
|
||||
server_window_normal_height_ =
|
||||
(int)(server_window_height_default_ * dpi_scale_);
|
||||
window_rounding_ = window_rounding_default_ * dpi_scale_;
|
||||
|
||||
SDL_SetWindowSize(main_window_, (int)main_window_width_,
|
||||
(int)main_window_height_);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(main_window_, false);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(main_window_, HitTestCallback, this);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(main_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
SetupFontAndStyle(&main_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
#if _WIN32
|
||||
SDL_PropertiesID props = SDL_GetWindowProperties(main_window_);
|
||||
HWND main_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
HICON tray_icon = LoadTrayIcon();
|
||||
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__APPLE__)
|
||||
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
|
||||
localization_language_index_);
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
tray_ = std::make_unique<LinuxTray>(
|
||||
main_window_, "CrossDesk", localization_language_index_, APP_EXIT_EVENT);
|
||||
#endif
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(main_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyMainWindow() {
|
||||
if (main_ctx_) {
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
}
|
||||
|
||||
if (main_renderer_) {
|
||||
SDL_DestroyRenderer(main_renderer_);
|
||||
}
|
||||
|
||||
if (main_window_) {
|
||||
SDL_DestroyWindow(main_window_);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateStreamWindow() {
|
||||
if (stream_window_created_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_width_ = (int)(stream_window_width_default_ * dpi_scale_);
|
||||
stream_window_height_ = (int)(stream_window_height_default_ * dpi_scale_);
|
||||
|
||||
stream_ctx_ = ImGui::CreateContext();
|
||||
if (!stream_ctx_) {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Stream Window", (int)stream_window_width_,
|
||||
(int)stream_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_TRANSPARENT,
|
||||
&stream_window_, &stream_renderer_)) {
|
||||
LOG_ERROR("Error creating stream_window_ and stream_renderer_: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
stream_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
|
||||
SDL_SetWindowResizable(stream_window_, true);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(stream_window_, HitTestCallback, this);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(stream_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
SetupFontAndStyle(&stream_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(stream_window_, stream_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(stream_renderer_);
|
||||
|
||||
// change props->stream_render_rect_
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED;
|
||||
event.window.windowID = SDL_GetWindowID(stream_window_);
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
stream_window_created_ = true;
|
||||
just_created_ = true;
|
||||
|
||||
stream_window_inited_ = true;
|
||||
LOG_INFO("Stream window inited");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyStreamWindow() {
|
||||
stream_window_width_ = (float)stream_window_width_default_;
|
||||
stream_window_height_ = (float)stream_window_height_default_;
|
||||
|
||||
if (stream_ctx_) {
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
}
|
||||
|
||||
if (stream_renderer_) {
|
||||
SDL_DestroyRenderer(stream_renderer_);
|
||||
stream_renderer_ = nullptr;
|
||||
}
|
||||
|
||||
if (stream_window_) {
|
||||
SDL_DestroyWindow(stream_window_);
|
||||
stream_window_ = nullptr;
|
||||
}
|
||||
|
||||
stream_window_created_ = false;
|
||||
focus_on_stream_window_ = false;
|
||||
stream_window_grabbed_ = false;
|
||||
control_mouse_ = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::CreateServerWindow() {
|
||||
if (server_window_created_) {
|
||||
return 0;
|
||||
}
|
||||
server_ctx_ = ImGui::CreateContext();
|
||||
if (!server_ctx_) {
|
||||
LOG_ERROR("Server context is null");
|
||||
return -1;
|
||||
}
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
if (!SDL_CreateWindowAndRenderer(
|
||||
"CrossDesk Server Window", (int)server_window_width_,
|
||||
(int)server_window_height_,
|
||||
SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_BORDERLESS |
|
||||
SDL_WINDOW_TRANSPARENT,
|
||||
&server_window_, &server_renderer_)) {
|
||||
LOG_ERROR("Error creating server_window_ and server_renderer_: {}",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
// Hide server window from the taskbar by making it a tool window.
|
||||
{
|
||||
SDL_PropertiesID server_props = SDL_GetWindowProperties(server_window_);
|
||||
HWND server_hwnd = (HWND)SDL_GetPointerProperty(
|
||||
server_props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
|
||||
|
||||
if (server_hwnd) {
|
||||
LONG_PTR ex_style = GetWindowLongPtr(server_hwnd, GWL_EXSTYLE);
|
||||
ex_style |= WS_EX_TOOLWINDOW;
|
||||
ex_style &= ~WS_EX_APPWINDOW;
|
||||
SetWindowLongPtr(server_hwnd, GWL_EXSTYLE, ex_style);
|
||||
|
||||
// Keep the server window above normal windows.
|
||||
SetWindowPos(server_hwnd, HWND_TOPMOST, 0, 0, 0, 0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
// Best-effort keep above other windows on X11.
|
||||
X11SetWindowAlwaysOnTop(server_window_);
|
||||
// Best-effort hide from taskbar on X11.
|
||||
X11SetWindowSkipTaskbar(server_window_);
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// Best-effort keep above other windows on macOS.
|
||||
MacSetWindowAlwaysOnTop(server_window_, true);
|
||||
// Best-effort exclude from Window menu / window cycling.
|
||||
MacSetWindowExcludedFromWindowMenu(server_window_, true);
|
||||
#endif
|
||||
|
||||
// Set window position to bottom-right corner
|
||||
SDL_Rect display_bounds;
|
||||
if (SDL_GetDisplayUsableBounds(SDL_GetDisplayForWindow(server_window_),
|
||||
&display_bounds)) {
|
||||
int window_x =
|
||||
display_bounds.x + display_bounds.w - (int)server_window_width_;
|
||||
int window_y =
|
||||
display_bounds.y + display_bounds.h - (int)server_window_height_;
|
||||
SDL_SetWindowPosition(server_window_, window_x, window_y);
|
||||
}
|
||||
|
||||
SDL_SetWindowResizable(server_window_, false);
|
||||
|
||||
SDL_SetRenderDrawBlendMode(server_renderer_, SDL_BLENDMODE_BLEND);
|
||||
|
||||
// for window region action
|
||||
SDL_SetWindowHitTest(server_window_, HitTestCallback, this);
|
||||
|
||||
SetupFontAndStyle(&server_windows_system_chinese_font_);
|
||||
|
||||
ImGuiStyle &style = ImGui::GetStyle();
|
||||
style.ScaleAllSizes(dpi_scale_);
|
||||
style.FontScaleDpi = dpi_scale_;
|
||||
|
||||
ImGui_ImplSDL3_InitForSDLRenderer(server_window_, server_renderer_);
|
||||
ImGui_ImplSDLRenderer3_Init(server_renderer_);
|
||||
|
||||
server_window_created_ = true;
|
||||
server_window_inited_ = true;
|
||||
|
||||
LOG_INFO("Server window inited");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyServerWindow() {
|
||||
if (server_ctx_) {
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
}
|
||||
|
||||
if (server_renderer_) {
|
||||
SDL_DestroyRenderer(server_renderer_);
|
||||
server_renderer_ = nullptr;
|
||||
}
|
||||
|
||||
if (server_window_) {
|
||||
SDL_DestroyWindow(server_window_);
|
||||
server_window_ = nullptr;
|
||||
}
|
||||
|
||||
server_window_created_ = false;
|
||||
server_window_inited_ = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::SetupFontAndStyle(ImFont **system_chinese_font_out) {
|
||||
float font_size = 32.0f;
|
||||
|
||||
// Setup Dear ImGui style
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
|
||||
io.IniFilename = NULL; // disable imgui.ini
|
||||
|
||||
// Build one merged atlas: UI font + icon font + multilingual fallback fonts.
|
||||
ImFontConfig config;
|
||||
config.FontDataOwnedByAtlas = false;
|
||||
config.MergeMode = false;
|
||||
|
||||
if (system_chinese_font_out) {
|
||||
*system_chinese_font_out = nullptr;
|
||||
}
|
||||
|
||||
ImFont *ui_font = nullptr;
|
||||
const ImWchar *multilingual_ranges = GetMultilingualGlyphRanges();
|
||||
|
||||
#if defined(_WIN32)
|
||||
const char *base_font_paths[] = {
|
||||
"C:/Windows/Fonts/msyh.ttc", "C:/Windows/Fonts/msyhbd.ttc",
|
||||
"C:/Windows/Fonts/segoeui.ttf", "C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc", nullptr};
|
||||
#elif defined(__APPLE__)
|
||||
const char *base_font_paths[] = {
|
||||
"/System/Library/Fonts/PingFang.ttc",
|
||||
"/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
|
||||
"/System/Library/Fonts/Supplemental/Arial.ttf",
|
||||
"/System/Library/Fonts/SFNS.ttf", nullptr};
|
||||
#else
|
||||
const char *base_font_paths[] = {
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
nullptr};
|
||||
#endif
|
||||
|
||||
for (int i = 0; base_font_paths[i] != nullptr && ui_font == nullptr; ++i) {
|
||||
if (!CanReadFontFile(base_font_paths[i])) {
|
||||
continue;
|
||||
}
|
||||
ui_font = io.Fonts->AddFontFromFileTTF(base_font_paths[i], font_size,
|
||||
&config, multilingual_ranges);
|
||||
if (ui_font != nullptr) {
|
||||
LOG_INFO("Loaded base UI font: {}", base_font_paths[i]);
|
||||
}
|
||||
}
|
||||
if (!ui_font) {
|
||||
ui_font = io.Fonts->AddFontDefault(&config);
|
||||
}
|
||||
|
||||
if (!ui_font) {
|
||||
LOG_WARN("Failed to initialize base UI font");
|
||||
ImGui::StyleColorsLight();
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImFontConfig icon_config = config;
|
||||
icon_config.MergeMode = true;
|
||||
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
|
||||
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len,
|
||||
font_size, &icon_config, icon_ranges);
|
||||
|
||||
io.FontDefault = ui_font;
|
||||
if (system_chinese_font_out) {
|
||||
*system_chinese_font_out = ui_font;
|
||||
}
|
||||
|
||||
ImGui::StyleColorsLight();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyMainWindowContext() {
|
||||
if (!main_ctx_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(main_ctx_);
|
||||
main_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyStreamWindowContext() {
|
||||
if (!stream_ctx_) {
|
||||
stream_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
stream_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(stream_ctx_);
|
||||
stream_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DestroyServerWindowContext() {
|
||||
if (!server_ctx_) {
|
||||
server_window_inited_ = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
server_window_inited_ = false;
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDLRenderer3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext(server_ctx_);
|
||||
server_ctx_ = nullptr;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,236 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int GuiApplication::DrawMainWindow() {
|
||||
if (!main_ctx_) {
|
||||
LOG_ERROR("Main context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(main_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("MainRender", nullptr,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoDocking);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
TitleBar(true);
|
||||
|
||||
MainWindow();
|
||||
|
||||
UpdateNotificationWindow();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
PortableServiceInstallWindow();
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (show_request_permission_window_) {
|
||||
RequestPermissionWindow();
|
||||
}
|
||||
#endif
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Rendering
|
||||
(void)io;
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(main_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(main_renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(main_renderer_);
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), main_renderer_);
|
||||
SDL_RenderPresent(main_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DrawStreamWindow() {
|
||||
if (!stream_ctx_) {
|
||||
LOG_ERROR("Stream context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
ImGui::SetCurrentContext(stream_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
StreamWindow();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
float stream_title_window_height =
|
||||
fullscreen_button_pressed_ ? 0 : title_bar_height_;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
// Set minimum window size to 0 to allow exact height control
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, stream_title_window_height),
|
||||
ImGuiCond_Always);
|
||||
ImGui::Begin("StreamTitleWindow", nullptr,
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ImGuiWindowFlags_NoDocking);
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (!fullscreen_button_pressed_) {
|
||||
TitleBar(false);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
|
||||
// Rendering
|
||||
(void)io;
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(stream_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(stream_renderer_, 0, 0, 0, 255);
|
||||
SDL_RenderClear(stream_renderer_);
|
||||
|
||||
// std::shared_lock lock(remote_sessions_mutex_);
|
||||
for (auto &it : remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->tab_selected_) {
|
||||
SDL_FRect render_rect_f = {
|
||||
props->stream_render_rect_f_.x, props->stream_render_rect_f_.y,
|
||||
props->stream_render_rect_f_.w, props->stream_render_rect_f_.h};
|
||||
SDL_RenderTexture(stream_renderer_, props->stream_texture_, NULL,
|
||||
&render_rect_f);
|
||||
}
|
||||
}
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), stream_renderer_);
|
||||
SDL_RenderPresent(stream_renderer_);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int GuiApplication::DrawServerWindow() {
|
||||
if (!server_ctx_) {
|
||||
LOG_ERROR("Server context is null");
|
||||
return -1;
|
||||
}
|
||||
ImGui::SetCurrentContext(server_ctx_);
|
||||
ImGui_ImplSDLRenderer3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
ImGuiIO &io = ImGui::GetIO();
|
||||
server_window_width_ = io.DisplaySize.x;
|
||||
server_window_height_ = io.DisplaySize.y;
|
||||
|
||||
ServerWindow();
|
||||
ImGui::Render();
|
||||
SDL_SetRenderScale(server_renderer_, io.DisplayFramebufferScale.x,
|
||||
io.DisplayFramebufferScale.y);
|
||||
SDL_SetRenderDrawColor(server_renderer_, 0, 0, 0, 0);
|
||||
SDL_RenderClear(server_renderer_);
|
||||
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), server_renderer_);
|
||||
SDL_RenderPresent(server_renderer_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GuiApplication::MinimizeMainWindowToTray() {
|
||||
if (!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
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-06-14
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _LAYOUT_STYLE_H_
|
||||
#define _LAYOUT_STYLE_H_
|
||||
|
||||
#define MENU_WINDOW_WIDTH_CN 300 * dpi_scale_
|
||||
#define MENU_WINDOW_HEIGHT_CN 280 * dpi_scale_
|
||||
#define LOCAL_WINDOW_WIDTH_CN 300 * dpi_scale_
|
||||
#define LOCAL_WINDOW_HEIGHT_CN 280 * dpi_scale_
|
||||
#define REMOTE_WINDOW_WIDTH_CN 300 * dpi_scale_
|
||||
#define REMOTE_WINDOW_HEIGHT_CN 280 * dpi_scale_
|
||||
#define MENU_WINDOW_WIDTH_EN 190 * dpi_scale_
|
||||
#define MENU_WINDOW_HEIGHT_EN 245 * dpi_scale_
|
||||
#define IPUT_WINDOW_WIDTH 160 * dpi_scale_
|
||||
#define INPUT_WINDOW_PADDING_CN 66 * dpi_scale_
|
||||
#define INPUT_WINDOW_PADDING_EN 96 * dpi_scale_
|
||||
#define SETTINGS_WINDOW_WIDTH_CN 202 * dpi_scale_
|
||||
#define SETTINGS_WINDOW_WIDTH_EN 248 * dpi_scale_
|
||||
#if USE_CUDA
|
||||
#if _WIN32
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 405 * dpi_scale_
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 405 * dpi_scale_
|
||||
#else
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
|
||||
#endif
|
||||
#else
|
||||
#if _WIN32
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 375 * dpi_scale_
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 375 * dpi_scale_
|
||||
#else
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 345 * dpi_scale_
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 345 * dpi_scale_
|
||||
#endif
|
||||
#endif
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195 * dpi_scale_
|
||||
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
|
||||
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
|
||||
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
|
||||
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
|
||||
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
|
||||
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
|
||||
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120 * dpi_scale_
|
||||
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167 * dpi_scale_
|
||||
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_AUTOSTART_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_DAEMON_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_DAEMON_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137 * dpi_scale_
|
||||
#define SETTINGS_SELECT_WINDOW_WIDTH 73 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130 * dpi_scale_
|
||||
#define SETTINGS_OK_BUTTON_PADDING_CN 65 * dpi_scale_
|
||||
#define SETTINGS_OK_BUTTON_PADDING_EN 83 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78 * dpi_scale_
|
||||
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91 * dpi_scale_
|
||||
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162 * dpi_scale_
|
||||
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146 * dpi_scale_
|
||||
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120 * dpi_scale_
|
||||
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130 * dpi_scale_
|
||||
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125 * dpi_scale_
|
||||
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260 * dpi_scale_
|
||||
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125 * dpi_scale_
|
||||
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90 * dpi_scale_
|
||||
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210 * dpi_scale_
|
||||
#endif
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-06-14
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _LAYOUT_STYLE_H_
|
||||
#define _LAYOUT_STYLE_H_
|
||||
|
||||
#define TITLE_BAR_HEIGHT 0.0625f
|
||||
#define TITLE_BAR_BUTTON_WIDTH 0.0625f
|
||||
#define TITLE_BAR_BUTTON_HEIGHT 0.0625f
|
||||
#define STATUS_BAR_HEIGHT 0.05f
|
||||
#define MENU_WINDOW_WIDTH_CN 300
|
||||
#define MENU_WINDOW_HEIGHT_CN 280
|
||||
#define LOCAL_WINDOW_WIDTH_CN 300
|
||||
#define LOCAL_WINDOW_HEIGHT_CN 280
|
||||
#define REMOTE_WINDOW_WIDTH_CN 300
|
||||
#define REMOTE_WINDOW_HEIGHT_CN 280
|
||||
#define MENU_WINDOW_WIDTH_EN 190
|
||||
#define MENU_WINDOW_HEIGHT_EN 245
|
||||
#define IPUT_WINDOW_WIDTH 160
|
||||
#define INPUT_WINDOW_PADDING_CN 66
|
||||
#define INPUT_WINDOW_PADDING_EN 96
|
||||
#define SETTINGS_WINDOW_WIDTH_CN 202
|
||||
#define SETTINGS_WINDOW_WIDTH_EN 248
|
||||
#if USE_CUDA
|
||||
#if _WIN32
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 405
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 405
|
||||
#else
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 375
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 375
|
||||
#endif
|
||||
#else
|
||||
#if _WIN32
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 375
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 375
|
||||
#else
|
||||
#define SETTINGS_WINDOW_HEIGHT_CN 345
|
||||
#define SETTINGS_WINDOW_HEIGHT_EN 345
|
||||
#endif
|
||||
#endif
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
|
||||
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_EN 195
|
||||
#define LANGUAGE_SELECT_WINDOW_PADDING_CN 120
|
||||
#define LANGUAGE_SELECT_WINDOW_PADDING_EN 167
|
||||
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_CN 120
|
||||
#define VIDEO_QUALITY_SELECT_WINDOW_PADDING_EN 167
|
||||
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_CN 120
|
||||
#define VIDEO_FRAME_RATE_SELECT_WINDOW_PADDING_EN 167
|
||||
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_CN 120
|
||||
#define VIDEO_ENCODE_FORMAT_SELECT_WINDOW_PADDING_EN 167
|
||||
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_AUTOSTART_PADDING_CN 171
|
||||
#define ENABLE_AUTOSTART_PADDING_EN 218
|
||||
#define ENABLE_DAEMON_PADDING_CN 171
|
||||
#define ENABLE_DAEMON_PADDING_EN 218
|
||||
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
|
||||
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
|
||||
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90
|
||||
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_EN 137
|
||||
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_CN 90
|
||||
#define SELF_HOSTED_SERVER_PORT_INPUT_BOX_PADDING_EN 137
|
||||
#define SETTINGS_SELECT_WINDOW_WIDTH 73
|
||||
#define SELF_HOSTED_SERVER_INPUT_WINDOW_WIDTH 130
|
||||
#define SETTINGS_OK_BUTTON_PADDING_CN 65
|
||||
#define SETTINGS_OK_BUTTON_PADDING_EN 83
|
||||
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
|
||||
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
|
||||
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162
|
||||
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146
|
||||
#define UPDATE_NOTIFICATION_RESERVED_HEIGHT 120
|
||||
#define REQUEST_PERMISSION_WINDOW_WIDTH_CN 130
|
||||
#define REQUEST_PERMISSION_WINDOW_HEIGHT_CN 125
|
||||
#define REQUEST_PERMISSION_WINDOW_WIDTH_EN 260
|
||||
#define REQUEST_PERMISSION_WINDOW_HEIGHT_EN 125
|
||||
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_CN 90
|
||||
#define REQUEST_PERMISSION_WINDOW_CHECKBOX_PADDING_EN 210
|
||||
#endif
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_H_
|
||||
#define _LOCALIZATION_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "localization_data.h"
|
||||
|
||||
#if _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
struct LanguageOption {
|
||||
std::string code;
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
class LocalizedString {
|
||||
public:
|
||||
constexpr explicit LocalizedString(const char* key) : key_(key) {}
|
||||
const std::string& operator[](int language_index) const;
|
||||
|
||||
private:
|
||||
const char* key_;
|
||||
};
|
||||
|
||||
inline const std::vector<LanguageOption>& GetSupportedLanguages() {
|
||||
static const std::vector<LanguageOption> kSupportedLanguages = {
|
||||
{"zh-CN", reinterpret_cast<const char*>(u8"中文")},
|
||||
{"en-US", "English"},
|
||||
{"ru-RU", reinterpret_cast<const char*>(u8"Русский")}};
|
||||
return kSupportedLanguages;
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline int ClampLanguageIndex(int language_index) {
|
||||
if (language_index >= 0 &&
|
||||
language_index < static_cast<int>(GetSupportedLanguages().size())) {
|
||||
return language_index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
using TranslationTable =
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<std::string, std::string>>;
|
||||
|
||||
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
|
||||
const TranslationRow& row) {
|
||||
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
|
||||
{"en-US", reinterpret_cast<const char*>(row.en)},
|
||||
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
|
||||
}
|
||||
|
||||
inline TranslationTable BuildTranslationTable() {
|
||||
TranslationTable table;
|
||||
for (const auto& row : kTranslationRows) {
|
||||
table[row.key] = MakeLocalizedValues(row);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const TranslationTable& GetTranslationTable() {
|
||||
static const TranslationTable table = BuildTranslationTable();
|
||||
return table;
|
||||
}
|
||||
|
||||
inline const std::string& GetTranslatedText(const std::string& key,
|
||||
int language_index) {
|
||||
static const std::string kEmptyText = "";
|
||||
|
||||
const auto& table = GetTranslationTable();
|
||||
const auto key_it = table.find(key);
|
||||
if (key_it == table.end()) {
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
const auto& localized_values = key_it->second;
|
||||
const std::string& language_code =
|
||||
GetSupportedLanguages()[ClampLanguageIndex(language_index)].code;
|
||||
|
||||
const auto exact_it = localized_values.find(language_code);
|
||||
if (exact_it != localized_values.end()) {
|
||||
return exact_it->second;
|
||||
}
|
||||
|
||||
const auto english_it = localized_values.find("en-US");
|
||||
if (english_it != localized_values.end()) {
|
||||
return english_it->second;
|
||||
}
|
||||
|
||||
const auto chinese_it = localized_values.find("zh-CN");
|
||||
if (chinese_it != localized_values.end()) {
|
||||
return chinese_it->second;
|
||||
}
|
||||
|
||||
return kEmptyText;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline const std::string& LocalizedString::operator[](
|
||||
int language_index) const {
|
||||
return detail::GetTranslatedText(key_, language_index);
|
||||
}
|
||||
|
||||
inline std::string FormatDisplayLabel(size_t display_index,
|
||||
const std::string& display_name,
|
||||
int language_index) {
|
||||
const std::string number = std::to_string(display_index + 1);
|
||||
std::string label =
|
||||
detail::GetTranslatedText("display_screen", language_index) + " " +
|
||||
number;
|
||||
const std::string fallback_name = "Display" + number;
|
||||
if (!display_name.empty() && display_name != fallback_name) {
|
||||
label += " (" + display_name + ")";
|
||||
}
|
||||
return label;
|
||||
}
|
||||
|
||||
#define CROSSDESK_DECLARE_LOCALIZED_STRING(name, zh, en, ru) \
|
||||
inline const LocalizedString name(#name);
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_LOCALIZED_STRING)
|
||||
#undef CROSSDESK_DECLARE_LOCALIZED_STRING
|
||||
|
||||
#if _WIN32
|
||||
inline const wchar_t* GetWindowsLocalizedLabel(
|
||||
int language_index, const char* key, const wchar_t* fallback,
|
||||
std::vector<std::wstring>& cache) {
|
||||
const int normalized_index = detail::ClampLanguageIndex(language_index);
|
||||
std::wstring& cached_text = cache[normalized_index];
|
||||
if (!cached_text.empty()) {
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
const std::string& utf8_text =
|
||||
detail::GetTranslatedText(key, normalized_index);
|
||||
if (utf8_text.empty()) {
|
||||
cached_text = fallback;
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
int wide_length =
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1, nullptr, 0);
|
||||
if (wide_length <= 0) {
|
||||
cached_text = fallback;
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
cached_text.resize(static_cast<size_t>(wide_length));
|
||||
if (MultiByteToWideChar(CP_UTF8, 0, utf8_text.c_str(), -1,
|
||||
cached_text.data(), wide_length) <= 0) {
|
||||
cached_text = fallback;
|
||||
return cached_text.c_str();
|
||||
}
|
||||
cached_text.pop_back();
|
||||
return cached_text.c_str();
|
||||
}
|
||||
|
||||
inline const wchar_t* GetShowMainWindowLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
return GetWindowsLocalizedLabel(language_index, "show_main_window",
|
||||
L"Show Main Window", cache);
|
||||
}
|
||||
|
||||
inline const wchar_t* GetSettingsLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
return GetWindowsLocalizedLabel(language_index, "settings", L"Settings",
|
||||
cache);
|
||||
}
|
||||
|
||||
inline const wchar_t* GetExitProgramLabel(int language_index) {
|
||||
static std::vector<std::wstring> cache(GetSupportedLanguages().size());
|
||||
return GetWindowsLocalizedLabel(language_index, "exit_program", L"Exit",
|
||||
cache);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2024-05-29
|
||||
* Copyright (c) 2024 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _LOCALIZATION_DATA_H_
|
||||
#define _LOCALIZATION_DATA_H_
|
||||
|
||||
namespace crossdesk {
|
||||
namespace localization {
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct TranslationRow {
|
||||
const char* key;
|
||||
const void* zh;
|
||||
const void* en;
|
||||
const void* ru;
|
||||
};
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
u8"Скопировано в буфер обмена") \
|
||||
X(password, u8"密码", "Password", u8"Пароль") \
|
||||
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||
u8"Удаленный рабочий стол") \
|
||||
X(device_name, u8"设备名称", "Device Name", u8"Имя устройства") \
|
||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||
u8"Недавние подключения") \
|
||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||
X(display_screen, u8"显示屏", "Display", u8"Экран") \
|
||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||
u8"Развернуть панель управления") \
|
||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||
u8"Свернуть панель управления") \
|
||||
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
||||
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
|
||||
u8"Показать статистику трафика") \
|
||||
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
|
||||
u8"Скрыть статистику трафика") \
|
||||
X(video, u8"视频", "Video", u8"Видео") \
|
||||
X(audio, u8"音频", "Audio", u8"Аудио") \
|
||||
X(data, u8"数据", "Data", u8"Данные") \
|
||||
X(total, u8"总计", "Total", u8"Итого") \
|
||||
X(in, u8"输入", "In", u8"Вход") \
|
||||
X(out, u8"输出", "Out", u8"Выход") \
|
||||
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
||||
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
||||
u8"Выйти из полноэкранного режима") \
|
||||
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
|
||||
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
|
||||
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||
"Remote lock screen visible, send SAS", \
|
||||
u8"Видна блокировка, отправьте SAS") \
|
||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||
"Remote Windows service unavailable", \
|
||||
u8"Служба Windows на удаленной стороне недоступна") \
|
||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||
X(windows_service_setup_message, \
|
||||
u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
|
||||
u8"Service。\n未安装该服务不影响 CrossDesk " \
|
||||
u8"正常使用,仅无法在锁屏状态下控制本机。", \
|
||||
"To support remote control of this device while it is locked, CrossDesk " \
|
||||
"Service must be installed with administrator permission.\nWithout this " \
|
||||
"service, CrossDesk still works normally; only lock-screen control of " \
|
||||
"this computer is unavailable.", \
|
||||
u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
|
||||
u8"блокировки, необходимо установить CrossDesk Service с правами " \
|
||||
u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
|
||||
u8"нормально; будет недоступно только управление этим компьютером на " \
|
||||
u8"экране блокировки.") \
|
||||
X(install_windows_service, u8"安装", "Install", u8"Установить") \
|
||||
X(windows_service_settings_label, u8"锁屏控制服务:", \
|
||||
"Lock Screen Service:", u8"Служба блокировки экрана:") \
|
||||
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
|
||||
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
|
||||
u8"Больше не напоминать") \
|
||||
X(windows_service_prompt_suppressed_message, \
|
||||
u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
|
||||
"You will not be reminded again. To enable remote control while locked " \
|
||||
"later, click Install in Settings.", \
|
||||
u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
|
||||
u8"экране блокировки, нажмите «Установить» в настройках.") \
|
||||
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||
u8"Установка службы...") \
|
||||
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||
"Service installed and started", u8"Служба установлена и запущена") \
|
||||
X(windows_service_install_failed, \
|
||||
u8"服务安装失败。请确认 " \
|
||||
u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
|
||||
u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
|
||||
"Service installation failed. Make sure CrossDesk.exe, " \
|
||||
"crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
|
||||
"portable folder, then approve the administrator prompt.", \
|
||||
u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
|
||||
u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
|
||||
u8"одной папке портативной версии, затем подтвердите запрос прав " \
|
||||
u8"администратора.") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||
X(video_encode_format, u8"视频编码格式:", \
|
||||
"Video Encode Format:", u8"Формат кодека видео:") \
|
||||
X(av1, u8"AV1", "AV1", "AV1") \
|
||||
X(h264, u8"H.264", "H.264", "H.264") \
|
||||
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
|
||||
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
||||
X(enable_turn, u8"启用中继服务:", \
|
||||
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
||||
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
||||
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
||||
u8"Конфигурация self-hosted") \
|
||||
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
||||
u8"Настройки self-hosted") \
|
||||
X(self_hosted_server_address, u8"服务器地址:", \
|
||||
"Server Address:", u8"Адрес сервера:") \
|
||||
X(self_hosted_server_port, u8"信令服务端口:", \
|
||||
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
||||
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
||||
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
||||
X(ok, u8"确认", "OK", u8"ОК") \
|
||||
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
||||
X(new_password, u8"请输入六位密码:", \
|
||||
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
|
||||
X(input_password, u8"请输入密码:", \
|
||||
"Please input password:", u8"Введите пароль:") \
|
||||
X(validate_password, u8"验证密码中...", "Validate password ...", \
|
||||
u8"Проверка пароля...") \
|
||||
X(reinput_password, u8"请重新输入密码", "Please input password again", \
|
||||
u8"Повторно введите пароль") \
|
||||
X(remember_password, u8"记住密码", "Remember password", \
|
||||
u8"Запомнить пароль") \
|
||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||
u8"Нет подключения к серверу") \
|
||||
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
|
||||
"Certificate verification failed. Reinstall the self-hosted root " \
|
||||
"certificate.", \
|
||||
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
|
||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||
u8"P2P отключено") \
|
||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||
u8"Подключение P2P...") \
|
||||
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
|
||||
u8"Сбор кандидатов...") \
|
||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||
u8"Получение изображения...") \
|
||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
||||
X(about, u8"关于", "About", u8"О программе") \
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
X(access_website, u8"访问官网: ", \
|
||||
"Access Website: ", u8"Официальный сайт: ") \
|
||||
X(update, u8"更新", "Update", u8"Обновить") \
|
||||
X(connection_alias, u8"修改名称", "Edit Alias", \
|
||||
u8"Изменить имя подключения") \
|
||||
X(delete_connection, u8"删除连接", "Delete Connection", \
|
||||
u8"Удалить подключение") \
|
||||
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
|
||||
u8"Подключиться") \
|
||||
X(input_connection_alias, u8"请输入连接名称:", \
|
||||
"Please input connection name:", u8"Введите имя подключения:") \
|
||||
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
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(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(show_main_window, u8"显示主界面", "Show Main Window", \
|
||||
u8"Показать главное окно") \
|
||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||
|
||||
inline constexpr TranslationRow kTranslationRows[] = {
|
||||
#define CROSSDESK_DECLARE_TRANSLATION_ROW(name, zh, en, ru) {#name, zh, en, ru},
|
||||
CROSSDESK_LOCALIZATION_ALL(CROSSDESK_DECLARE_TRANSLATION_ROW)
|
||||
#undef CROSSDESK_DECLARE_TRANSLATION_ROW
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
} // namespace localization
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,172 @@
|
||||
#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);
|
||||
}
|
||||
|
||||
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 (event_type_ != 0 && !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,584 @@
|
||||
#include "features/devices/session_device_manager.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
||||
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
bool SessionDeviceManager::ShouldSendCapturedFrame(
|
||||
std::chrono::steady_clock::time_point now, int fps) {
|
||||
const auto interval =
|
||||
std::chrono::nanoseconds(std::chrono::seconds(1)) / (fps > 0 ? fps : 1);
|
||||
if (next_frame_deadline_ == std::chrono::steady_clock::time_point{}) {
|
||||
next_frame_deadline_ = now;
|
||||
}
|
||||
if (now + kFrameDeadlineTolerance < next_frame_deadline_) {
|
||||
return false;
|
||||
}
|
||||
next_frame_deadline_ += interval;
|
||||
if (next_frame_deadline_ <= now) {
|
||||
next_frame_deadline_ = now + interval;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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_ = {};
|
||||
next_frame_deadline_ = {};
|
||||
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
LOG_INFO("Init screen capturer with {} fps", fps);
|
||||
display_info_list_.clear();
|
||||
registered_display_stream_count_ = 0;
|
||||
last_video_frame_stream_id_.clear();
|
||||
invalid_video_stream_id_logged_ = false;
|
||||
|
||||
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 = std::chrono::steady_clock::now();
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool has_previous_frame =
|
||||
last_frame_time_.time_since_epoch().count() != 0;
|
||||
const auto duration_ms =
|
||||
has_previous_frame
|
||||
? std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now_time - last_frame_time_)
|
||||
.count()
|
||||
: 0;
|
||||
|
||||
std::vector<std::string> connected_remote_ids;
|
||||
{
|
||||
std::shared_lock lock(owner_.connection_status_mutex_);
|
||||
connected_remote_ids.reserve(owner_.connection_status_.size());
|
||||
for (const auto &[remote_id, status] : owner_.connection_status_) {
|
||||
if (status == ConnectionStatus::Connected) {
|
||||
connected_remote_ids.push_back(remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Capture can still deliver frames while ICE is gathering or after
|
||||
// the final controller disconnects. Do not broadcast those frames to
|
||||
// MiniRTC: a broadcast also reaches newly joining peers whose ICE
|
||||
// transport is not ready yet.
|
||||
if (connected_remote_ids.empty()) {
|
||||
last_frame_time_ = now_time;
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string stream_id = ResolveDisplayStreamId(
|
||||
display_name, registered_display_stream_count_, -1,
|
||||
last_video_frame_stream_id_);
|
||||
if (stream_id.empty()) {
|
||||
if (!invalid_video_stream_id_logged_) {
|
||||
LOG_ERROR(
|
||||
"Drop captured frames with an empty or unregistered video "
|
||||
"stream id, reported='{}', registered_streams={}",
|
||||
display_name ? display_name : "",
|
||||
registered_display_stream_count_);
|
||||
invalid_video_stream_id_logged_ = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
invalid_video_stream_id_logged_ = false;
|
||||
const bool resumed_after_gap =
|
||||
has_previous_frame && duration_ms >= 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_ms, 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_);
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
}
|
||||
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;
|
||||
}
|
||||
registered_display_stream_count_ = display_info_list_.size();
|
||||
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_window_events_ = false;
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (!owner_.EnsureMacAccessibilityPermission()) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!keyboard_capturer_) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"keyboard capturer is nullptr, falling back to Slint 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 *devices = static_cast<SessionDeviceManager *>(user_ptr);
|
||||
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
},
|
||||
this);
|
||||
if (hook_ret != 0) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"Start keyboard capturer failed, falling back to Slint keyboard events");
|
||||
} else {
|
||||
LOG_INFO("Start keyboard capturer with native input");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SessionDeviceManager::StopKeyboardCapturer() {
|
||||
if (owner_.keyboard_capturer_uses_window_events_) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = false;
|
||||
LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (keyboard_capturer_) {
|
||||
keyboard_capturer_->Unhook();
|
||||
ClearCapturedKeyboardInput();
|
||||
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_) {
|
||||
DrainCapturedKeyboardInput();
|
||||
owner_.keyboard_.SendHeartbeat(false);
|
||||
}
|
||||
} else if (owner_.keyboard_capturer_is_started_) {
|
||||
owner_.keyboard_.ForceReleasePressedKeys();
|
||||
StopKeyboardCapturer();
|
||||
owner_.keyboard_capturer_is_started_ = false;
|
||||
}
|
||||
|
||||
owner_.keyboard_.CheckRemoteTimeouts();
|
||||
}
|
||||
|
||||
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
|
||||
bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
|
||||
captured_keyboard_inputs_.pop_front();
|
||||
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
|
||||
}
|
||||
captured_keyboard_inputs_.push_back(
|
||||
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||
std::deque<CapturedKeyboardInput> inputs;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
inputs.swap(captured_keyboard_inputs_);
|
||||
}
|
||||
for (const CapturedKeyboardInput &input : inputs) {
|
||||
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||
input.scan_code, input.extended);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionDeviceManager::ClearCapturedKeyboardInput() {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
captured_keyboard_inputs_.clear();
|
||||
}
|
||||
|
||||
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,96 @@
|
||||
#ifndef CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
#define CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#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:
|
||||
struct CapturedKeyboardInput {
|
||||
int key_code = 0;
|
||||
bool is_down = false;
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
};
|
||||
|
||||
void QueueCapturedKeyboardInput(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended);
|
||||
void DrainCapturedKeyboardInput();
|
||||
void ClearCapturedKeyboardInput();
|
||||
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
||||
int fps);
|
||||
|
||||
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_;
|
||||
size_t registered_display_stream_count_ = 0;
|
||||
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
std::chrono::steady_clock::time_point last_frame_time_{};
|
||||
std::chrono::steady_clock::time_point next_frame_deadline_{};
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool invalid_video_stream_id_logged_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_SESSION_DEVICE_MANAGER_H_
|
||||
@@ -0,0 +1,340 @@
|
||||
#include "features/file_transfer/file_transfer_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "filesystem_utf8.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 = PathFromUtf8(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;
|
||||
const auto utf8_name = file_path.filename().u8string();
|
||||
info.file_name.assign(reinterpret_cast<const char *>(utf8_name.data()),
|
||||
utf8_name.size());
|
||||
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) {
|
||||
FileTransferAck ack{};
|
||||
if (!DecodeFileTransferAck(data, size, &ack)) {
|
||||
LOG_ERROR("FileTransferAck: invalid payload, size={}", size);
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,54 @@
|
||||
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_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 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,365 @@
|
||||
#include "features/input/keyboard_controller.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#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,65 @@
|
||||
#ifndef CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
|
||||
#define CROSSDESK_GUI_KEYBOARD_CONTROLLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#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,622 @@
|
||||
#include "features/settings/settings_manager.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
|
||||
constexpr uint32_t kCacheV3Version = 3;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t CacheChecksum(const void *data, size_t size) {
|
||||
const auto *bytes = static_cast<const unsigned char *>(data);
|
||||
uint32_t hash = 2166136261u;
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
hash ^= bytes[i];
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::filesystem::path TemporaryPathFor(
|
||||
const std::filesystem::path &target) {
|
||||
const auto timestamp = std::chrono::steady_clock::now()
|
||||
.time_since_epoch()
|
||||
.count();
|
||||
const auto thread_id =
|
||||
std::hash<std::thread::id>{}(std::this_thread::get_id());
|
||||
std::filesystem::path temporary = target;
|
||||
temporary += ".tmp-" + std::to_string(timestamp) + "-" +
|
||||
std::to_string(thread_id);
|
||||
return temporary;
|
||||
}
|
||||
|
||||
bool FlushFileToDisk(FILE *file) {
|
||||
if (!file || std::fflush(file) != 0) {
|
||||
return false;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
return _commit(_fileno(file)) == 0;
|
||||
#else
|
||||
return fsync(fileno(file)) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ReplaceFileAtomically(const std::filesystem::path &temporary,
|
||||
const std::filesystem::path &target) {
|
||||
#if defined(_WIN32)
|
||||
return MoveFileExW(temporary.c_str(), target.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
|
||||
#else
|
||||
if (::rename(temporary.c_str(), target.c_str()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path parent = target.parent_path().empty()
|
||||
? std::filesystem::path(".")
|
||||
: target.parent_path();
|
||||
int directory_flags = O_RDONLY;
|
||||
#if defined(O_DIRECTORY)
|
||||
directory_flags |= O_DIRECTORY;
|
||||
#endif
|
||||
const int directory = open(parent.c_str(), directory_flags);
|
||||
if (directory >= 0) {
|
||||
const bool synced = fsync(directory) == 0;
|
||||
close(directory);
|
||||
return synced;
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool WriteFileAtomically(const std::filesystem::path &target,
|
||||
const void *data, size_t size) {
|
||||
std::error_code ec;
|
||||
const std::filesystem::path parent = target.parent_path();
|
||||
if (!parent.empty()) {
|
||||
std::filesystem::create_directories(parent, ec);
|
||||
if (ec) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const std::filesystem::path temporary = TemporaryPathFor(target);
|
||||
#if defined(_WIN32)
|
||||
FILE *file = _wfopen(temporary.c_str(), L"wb");
|
||||
#else
|
||||
FILE *file = std::fopen(temporary.c_str(), "wb");
|
||||
#endif
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool written = std::fwrite(data, 1, size, file) == size;
|
||||
const bool flushed = written && FlushFileToDisk(file);
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
if (!written || !flushed || !closed ||
|
||||
!ReplaceFileAtomically(temporary, target)) {
|
||||
std::filesystem::remove(temporary, ec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
|
||||
|
||||
int SettingsManager::Save() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
return SaveLocked();
|
||||
}
|
||||
|
||||
int SettingsManager::SaveLocked() {
|
||||
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_v3_.magic = kCacheV3Magic;
|
||||
cache_v3_.version = kCacheV3Version;
|
||||
cache_v3_.base = cache_v2_;
|
||||
cache_v3_.pending_identity[sizeof(cache_v3_.pending_identity) - 1] = '\0';
|
||||
cache_v3_.pending_server_host[sizeof(cache_v3_.pending_server_host) - 1] =
|
||||
'\0';
|
||||
cache_v3_.pending_request_id[sizeof(cache_v3_.pending_request_id) - 1] =
|
||||
'\0';
|
||||
cache_v3_.checksum =
|
||||
CacheChecksum(&cache_v3_, offsetof(CacheV3, checksum));
|
||||
|
||||
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v3.enc",
|
||||
&cache_v3_, sizeof(cache_v3_))) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v2.enc",
|
||||
&cache_v2_, sizeof(cache_v2_))) {
|
||||
LOG_WARN("Failed to update legacy v2 credential cache");
|
||||
}
|
||||
|
||||
// Keep writing the legacy cache while older installations may still read it.
|
||||
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_));
|
||||
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache.enc",
|
||||
&cache_v1_, sizeof(cache_v1_))) {
|
||||
LOG_WARN("Failed to update legacy v1 credential cache");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SettingsManager::ReadV3Locked() {
|
||||
std::ifstream cache_file(owner_.cache_path_ + "/secure_cache_v3.enc",
|
||||
std::ios::binary);
|
||||
if (!cache_file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CacheV3 loaded{};
|
||||
cache_file.read(reinterpret_cast<char *>(&loaded), sizeof(loaded));
|
||||
if (cache_file.gcount() != static_cast<std::streamsize>(sizeof(loaded)) ||
|
||||
loaded.magic != kCacheV3Magic ||
|
||||
loaded.version != kCacheV3Version ||
|
||||
loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) {
|
||||
LOG_WARN("Ignore invalid v3 credential cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded.base.client_id_with_password
|
||||
[sizeof(loaded.base.client_id_with_password) - 1] = '\0';
|
||||
loaded.base.self_hosted_id[sizeof(loaded.base.self_hosted_id) - 1] = '\0';
|
||||
loaded.pending_identity[sizeof(loaded.pending_identity) - 1] = '\0';
|
||||
loaded.pending_server_host[sizeof(loaded.pending_server_host) - 1] = '\0';
|
||||
loaded.pending_request_id[sizeof(loaded.pending_request_id) - 1] = '\0';
|
||||
cache_v3_ = loaded;
|
||||
cache_v2_ = loaded.base;
|
||||
return true;
|
||||
}
|
||||
|
||||
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_);
|
||||
|
||||
const bool loaded_v3 = ReadV3Locked();
|
||||
if (loaded_v3 || 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));
|
||||
if (loaded_v3) {
|
||||
LOG_INFO("Load settings from v3 cache file");
|
||||
} else {
|
||||
cache_v3_ = {};
|
||||
SaveLocked();
|
||||
LOG_INFO("Migrated settings from v2 to v3 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));
|
||||
|
||||
cache_v3_ = {};
|
||||
SaveLocked();
|
||||
LOG_INFO("Migrated settings from v1 to v3 cache file");
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
|
||||
ActivateCachedPublicIdentity();
|
||||
|
||||
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();
|
||||
#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_;
|
||||
|
||||
LOG_INFO("Load settings from cache file");
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool SettingsManager::LoadCachedSelfHostedIdentity() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if ((!ReadV3Locked() && !ReadV2Locked()) ||
|
||||
cache_v2_.self_hosted_id[0] == '\0') {
|
||||
std::memset(owner_.self_hosted_id_, 0, sizeof(owner_.self_hosted_id_));
|
||||
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
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;
|
||||
}
|
||||
|
||||
bool SettingsManager::ActivateCachedPublicIdentity() {
|
||||
if (owner_.client_id_with_password_[0] == '\0') {
|
||||
std::memset(owner_.client_id_, 0, sizeof(owner_.client_id_));
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *at_pos = std::strchr(owner_.client_id_with_password_, '@');
|
||||
if (at_pos == nullptr) {
|
||||
CopyString(owner_.client_id_, owner_.client_id_with_password_);
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
} else {
|
||||
const std::string id(owner_.client_id_with_password_,
|
||||
at_pos - owner_.client_id_with_password_);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, at_pos + 1);
|
||||
}
|
||||
return owner_.client_id_[0] != '\0';
|
||||
}
|
||||
|
||||
void SettingsManager::ActivateIdentity(const char *identity,
|
||||
bool self_hosted) {
|
||||
if (!identity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self_hosted) {
|
||||
CopyString(owner_.self_hosted_id_, identity);
|
||||
} else {
|
||||
CopyString(owner_.client_id_with_password_, identity);
|
||||
}
|
||||
|
||||
const char *at_pos = std::strchr(identity, '@');
|
||||
if (at_pos == nullptr) {
|
||||
CopyString(owner_.client_id_, identity);
|
||||
std::memset(owner_.password_saved_, 0, sizeof(owner_.password_saved_));
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string id(identity, at_pos - identity);
|
||||
CopyString(owner_.client_id_, id.c_str());
|
||||
CopyString(owner_.password_saved_, at_pos + 1);
|
||||
}
|
||||
|
||||
bool SettingsManager::StagePendingPasswordChange(
|
||||
const std::string &identity, bool self_hosted,
|
||||
const std::string &server_host, int server_port,
|
||||
const std::string &request_id) {
|
||||
if (identity.empty() || identity.size() >= sizeof(cache_v3_.pending_identity) ||
|
||||
server_host.empty() ||
|
||||
server_host.size() >= sizeof(cache_v3_.pending_server_host) ||
|
||||
server_port <= 0 || request_id.empty() ||
|
||||
request_id.size() >= sizeof(cache_v3_.pending_request_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] != '\0' &&
|
||||
(cache_v3_.pending_self_hosted != self_hosted ||
|
||||
cache_v3_.pending_server_port != server_port ||
|
||||
server_host != cache_v3_.pending_server_host ||
|
||||
request_id != cache_v3_.pending_request_id)) {
|
||||
LOG_WARN("Refuse to overwrite an unresolved password change");
|
||||
return false;
|
||||
}
|
||||
const CacheV3 previous = cache_v3_;
|
||||
CopyString(cache_v3_.pending_identity, identity.c_str());
|
||||
CopyString(cache_v3_.pending_server_host, server_host.c_str());
|
||||
cache_v3_.pending_server_port = server_port;
|
||||
cache_v3_.pending_self_hosted = self_hosted;
|
||||
CopyString(cache_v3_.pending_request_id, request_id.c_str());
|
||||
if (SaveLocked() != 0) {
|
||||
cache_v3_ = previous;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string SettingsManager::PendingPasswordChangeIdentity(
|
||||
bool self_hosted, const std::string &server_host, int server_port) const {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] == '\0' ||
|
||||
cache_v3_.pending_self_hosted != self_hosted ||
|
||||
cache_v3_.pending_server_port != server_port ||
|
||||
server_host != cache_v3_.pending_server_host) {
|
||||
return {};
|
||||
}
|
||||
return cache_v3_.pending_identity;
|
||||
}
|
||||
|
||||
bool SettingsManager::PromotePendingPasswordChange() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] == '\0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const CacheV3 previous = cache_v3_;
|
||||
const std::string identity = cache_v3_.pending_identity;
|
||||
const bool self_hosted = cache_v3_.pending_self_hosted;
|
||||
ActivateIdentity(identity.c_str(), self_hosted);
|
||||
std::memset(cache_v3_.pending_identity, 0,
|
||||
sizeof(cache_v3_.pending_identity));
|
||||
std::memset(cache_v3_.pending_server_host, 0,
|
||||
sizeof(cache_v3_.pending_server_host));
|
||||
cache_v3_.pending_server_port = 0;
|
||||
cache_v3_.pending_self_hosted = false;
|
||||
std::memset(cache_v3_.pending_request_id, 0,
|
||||
sizeof(cache_v3_.pending_request_id));
|
||||
if (SaveLocked() != 0) {
|
||||
// The already-durable pending credential remains the recovery source on
|
||||
// disk. Keep it in memory as well so a reconnect in this process retries
|
||||
// the credential that the server has accepted.
|
||||
cache_v3_ = previous;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsManager::ClearPendingPasswordChange() {
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
if (cache_v3_.pending_identity[0] == '\0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const CacheV3 previous = cache_v3_;
|
||||
std::memset(cache_v3_.pending_identity, 0,
|
||||
sizeof(cache_v3_.pending_identity));
|
||||
std::memset(cache_v3_.pending_server_host, 0,
|
||||
sizeof(cache_v3_.pending_server_host));
|
||||
cache_v3_.pending_server_port = 0;
|
||||
cache_v3_.pending_self_hosted = false;
|
||||
std::memset(cache_v3_.pending_request_id, 0,
|
||||
sizeof(cache_v3_.pending_request_id));
|
||||
if (SaveLocked() != 0) {
|
||||
cache_v3_ = previous;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SettingsManager::PersistSelfHostedIdentity(const char *client_id) {
|
||||
if (!client_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache_mutex_);
|
||||
CopyString(owner_.self_hosted_id_, client_id);
|
||||
if (SaveLocked() != 0) {
|
||||
LOG_ERROR("Failed to persist self-hosted identity atomically");
|
||||
}
|
||||
}
|
||||
|
||||
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,114 @@
|
||||
#ifndef CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
#define CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#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();
|
||||
// Restores the public-server identity kept in the in-memory cache fields.
|
||||
// This must run when switching away from a self-hosted server so signal
|
||||
// callbacks are matched against the identity used by the replacement peer.
|
||||
bool ActivateCachedPublicIdentity();
|
||||
void PersistSelfHostedIdentity(const char *client_id);
|
||||
|
||||
// Password rotation is deliberately persisted in two phases. The active
|
||||
// credential remains usable while the pending credential records the value
|
||||
// that may already have been committed by the server. On the next launch the
|
||||
// caller can try the pending credential first and safely fall back to the
|
||||
// active credential when the request never reached the server.
|
||||
bool StagePendingPasswordChange(const std::string &identity,
|
||||
bool self_hosted,
|
||||
const std::string &server_host,
|
||||
int server_port,
|
||||
const std::string &request_id);
|
||||
std::string PendingPasswordChangeIdentity(
|
||||
bool self_hosted, const std::string &server_host,
|
||||
int server_port) const;
|
||||
bool PromotePendingPasswordChange();
|
||||
bool ClearPendingPasswordChange();
|
||||
|
||||
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];
|
||||
};
|
||||
|
||||
struct CacheV3 {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
CacheV2 base;
|
||||
char pending_identity[17];
|
||||
char pending_server_host[256];
|
||||
int pending_server_port;
|
||||
bool pending_self_hosted;
|
||||
char pending_request_id[64];
|
||||
uint32_t checksum;
|
||||
};
|
||||
|
||||
int SaveLocked();
|
||||
bool ReadV3Locked();
|
||||
bool ReadV2Locked();
|
||||
void ActivateIdentity(const char *identity, bool self_hosted);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
CacheV1 cache_v1_{};
|
||||
CacheV2 cache_v2_{};
|
||||
CacheV3 cache_v3_{};
|
||||
mutable std::mutex cache_mutex_;
|
||||
std::unordered_map<std::string, std::string> recent_connection_aliases_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_SETTINGS_MANAGER_H_
|
||||
@@ -0,0 +1,15 @@
|
||||
#include "render.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "application/gui_application.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
Render::Render() : application_(std::make_unique<GuiApplication>()) {}
|
||||
|
||||
Render::~Render() = default;
|
||||
|
||||
int Render::Run() { return application_->Run(); }
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef CROSSDESK_GUI_RENDER_H_
|
||||
#define CROSSDESK_GUI_RENDER_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class GuiApplication;
|
||||
|
||||
// Stable application-facing facade. The SDL application and feature
|
||||
// controllers remain private implementation details.
|
||||
class Render {
|
||||
public:
|
||||
Render();
|
||||
~Render();
|
||||
|
||||
Render(const Render &) = delete;
|
||||
Render &operator=(const Render &) = delete;
|
||||
|
||||
int Run();
|
||||
|
||||
private:
|
||||
std::unique_ptr<GuiApplication> application_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDER_H_
|
||||
@@ -0,0 +1,502 @@
|
||||
#include "rendering/slint_video_presenter.h"
|
||||
|
||||
#include <libyuv.h>
|
||||
#include <slint.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "nv12_scaler.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include "platform/macos/gui/metal_video_renderer.h"
|
||||
#else
|
||||
#if defined(_WIN32)
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <GL/gl.h>
|
||||
|
||||
#include "platform/common/gui/opengl_video_renderer.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
constexpr GLint kGlClampToEdge = 0x812F;
|
||||
constexpr float kStreamWindowCornerRadius = 12.0f;
|
||||
#else
|
||||
constexpr int kMetalAttachmentAttemptLimit = 30;
|
||||
#endif
|
||||
|
||||
struct VideoRenderSize {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
bool operator==(const VideoRenderSize&) const = default;
|
||||
};
|
||||
|
||||
VideoRenderSize FitVideoToRenderArea(int source_width, int source_height,
|
||||
int area_width, int area_height) {
|
||||
if (source_width <= 0 || source_height <= 0 || area_width <= 0 ||
|
||||
area_height <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const double scale =
|
||||
std::min({1.0, static_cast<double>(area_width) / source_width,
|
||||
static_cast<double>(area_height) / source_height});
|
||||
int width = std::max(2, static_cast<int>(std::floor(source_width * scale)));
|
||||
int height = std::max(2, static_cast<int>(std::floor(source_height * scale)));
|
||||
|
||||
width &= ~1;
|
||||
height &= ~1;
|
||||
return {std::min(width, source_width & ~1),
|
||||
std::min(height, source_height & ~1)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct SlintVideoPresenter::Impl {
|
||||
explicit Impl(VideoRenderer& renderer) : renderer(renderer) {}
|
||||
|
||||
VideoRenderer& renderer;
|
||||
slint::ComponentHandle<ui::StreamWindow>* stream = nullptr;
|
||||
StateProvider state_provider;
|
||||
DirtyCallback dirty_callback;
|
||||
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
||||
std::unordered_map<std::string, uint64_t> seeded_frame_sequence;
|
||||
std::vector<uint8_t> scaled_video_frame;
|
||||
std::vector<uint8_t> scaled_video_frame_scratch;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
int attachment_attempts = 0;
|
||||
bool skip_attachment_once = false;
|
||||
#else
|
||||
std::mutex gl_mutex;
|
||||
std::vector<uint8_t> gl_conversion_frame;
|
||||
std::vector<uint8_t> gl_pending_frame;
|
||||
uint32_t gl_texture = 0;
|
||||
VideoRenderSize gl_texture_size;
|
||||
VideoRenderSize gl_image_size;
|
||||
VideoRenderSize gl_pending_size;
|
||||
bool gl_pending_dirty = false;
|
||||
#endif
|
||||
|
||||
SurfaceState CurrentState() const {
|
||||
return state_provider ? state_provider() : SurfaceState{};
|
||||
}
|
||||
|
||||
void MarkDirty() const {
|
||||
if (dirty_callback) {
|
||||
dirty_callback();
|
||||
}
|
||||
}
|
||||
|
||||
bool MarkPresented(const std::string& remote_id, uint64_t sequence) {
|
||||
if (remote_id.empty() || sequence == 0 ||
|
||||
displayed_frame_sequence[remote_id] == sequence) {
|
||||
return false;
|
||||
}
|
||||
displayed_frame_sequence[remote_id] = sequence;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
void ConfigureOpenGlNotifier() {
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto error = (*stream)->window().set_rendering_notifier(
|
||||
[this](slint::RenderingState state, slint::GraphicsAPI graphics_api) {
|
||||
if (graphics_api != slint::GraphicsAPI::NativeOpenGL) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(gl_mutex);
|
||||
auto* opengl_renderer =
|
||||
dynamic_cast<OpenGlVideoRenderer*>(&renderer);
|
||||
if (state == slint::RenderingState::RenderingSetup) {
|
||||
GLuint texture = 0;
|
||||
glGenTextures(1, &texture);
|
||||
if (texture == 0) {
|
||||
return;
|
||||
}
|
||||
GLint previous_texture = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, kGlClampToEdge);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, kGlClampToEdge);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||
gl_texture = texture;
|
||||
if (opengl_renderer && opengl_renderer->Setup()) {
|
||||
const std::string selected_stream =
|
||||
CurrentState().selected_stream;
|
||||
opengl_renderer->SetSelectedStream(selected_stream);
|
||||
seeded_frame_sequence.erase(selected_stream);
|
||||
MarkDirty();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::BeforeRendering && stream &&
|
||||
opengl_renderer && opengl_renderer->IsReady() &&
|
||||
(*stream)->get_native_video_enabled()) {
|
||||
const SurfaceState surface = CurrentState();
|
||||
const auto window_size = (*stream)->window().size();
|
||||
const float scale_factor =
|
||||
std::max(1.0f, (*stream)->window().scale_factor());
|
||||
const int top_inset =
|
||||
!surface.fullscreen && surface.tab_count > 1
|
||||
? static_cast<int>(std::lround(30.0f * scale_factor))
|
||||
: 0;
|
||||
const bool rounded_window = (*stream)->get_custom_titlebar() &&
|
||||
!surface.fullscreen &&
|
||||
!(*stream)->window().is_maximized();
|
||||
const int corner_radius =
|
||||
rounded_window ? static_cast<int>(std::lround(
|
||||
kStreamWindowCornerRadius * scale_factor))
|
||||
: 0;
|
||||
opengl_renderer->RenderLatest(
|
||||
surface.selected_stream, static_cast<int>(window_size.width),
|
||||
static_cast<int>(window_size.height), top_inset, corner_radius);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::BeforeRendering &&
|
||||
gl_texture != 0 && gl_pending_dirty &&
|
||||
!gl_pending_frame.empty()) {
|
||||
GLint previous_texture = 0;
|
||||
GLint previous_unpack_alignment = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_2D, &previous_texture);
|
||||
glGetIntegerv(GL_UNPACK_ALIGNMENT, &previous_unpack_alignment);
|
||||
glBindTexture(GL_TEXTURE_2D, gl_texture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
if (gl_texture_size != gl_pending_size) {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, gl_pending_size.width,
|
||||
gl_pending_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
gl_pending_frame.data());
|
||||
gl_texture_size = gl_pending_size;
|
||||
} else {
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, gl_pending_size.width,
|
||||
gl_pending_size.height, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
gl_pending_frame.data());
|
||||
}
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, previous_unpack_alignment);
|
||||
glBindTexture(GL_TEXTURE_2D, static_cast<GLuint>(previous_texture));
|
||||
gl_pending_dirty = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == slint::RenderingState::RenderingTeardown) {
|
||||
if (opengl_renderer) {
|
||||
opengl_renderer->Teardown();
|
||||
}
|
||||
if (gl_texture != 0) {
|
||||
const GLuint texture = gl_texture;
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
gl_texture = 0;
|
||||
gl_texture_size = {};
|
||||
gl_image_size = {};
|
||||
gl_pending_size = {};
|
||||
gl_pending_dirty = false;
|
||||
gl_pending_frame.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if (error.has_value()) {
|
||||
LOG_WARN("Slint OpenGL video renderer unavailable, using pixel buffers");
|
||||
} else {
|
||||
renderer.SetSelectedStream(CurrentState().selected_stream);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
SlintVideoPresenter::SlintVideoPresenter(VideoRenderer& renderer)
|
||||
: impl_(std::make_unique<Impl>(renderer)) {}
|
||||
|
||||
SlintVideoPresenter::~SlintVideoPresenter() = default;
|
||||
|
||||
void SlintVideoPresenter::PrepareWindow(
|
||||
slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||
StateProvider state_provider, DirtyCallback dirty_callback) {
|
||||
impl_->stream = &stream;
|
||||
impl_->state_provider = std::move(state_provider);
|
||||
impl_->dirty_callback = std::move(dirty_callback);
|
||||
#if defined(__APPLE__)
|
||||
stream->set_native_video_enabled(false);
|
||||
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
|
||||
impl_->skip_attachment_once = true;
|
||||
#else
|
||||
impl_->ConfigureOpenGlNotifier();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SlintVideoPresenter::EnsureAttached() {
|
||||
#if defined(__APPLE__)
|
||||
if (!impl_->stream || impl_->attachment_attempts <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (impl_->skip_attachment_once) {
|
||||
impl_->skip_attachment_once = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* metal_renderer =
|
||||
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||
if (!metal_renderer) {
|
||||
impl_->attachment_attempts = 0;
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
return false;
|
||||
}
|
||||
if (!metal_renderer->IsReady()) {
|
||||
impl_->attachment_attempts = 0;
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
return false;
|
||||
}
|
||||
if (metal_renderer->IsAttached()) {
|
||||
impl_->attachment_attempts = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (void* view = (*impl_->stream)->window().appkit_view()) {
|
||||
(*impl_->stream)->set_native_video_enabled(true);
|
||||
if (metal_renderer->Attach(view)) {
|
||||
impl_->attachment_attempts = 0;
|
||||
const SurfaceState surface = impl_->CurrentState();
|
||||
impl_->displayed_frame_sequence.erase(surface.selected_stream);
|
||||
impl_->seeded_frame_sequence.erase(surface.selected_stream);
|
||||
if (metal_renderer->SetSelectedStream(surface.selected_stream)) {
|
||||
impl_->MarkDirty();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
}
|
||||
|
||||
if (--impl_->attachment_attempts == 0) {
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
LOG_WARN("Unable to attach the Metal video surface; using CPU rendering");
|
||||
}
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SlintVideoPresenter::NeedsRedraw() const {
|
||||
#if defined(__APPLE__)
|
||||
const auto* metal_renderer =
|
||||
dynamic_cast<const MacMetalVideoRenderer*>(&impl_->renderer);
|
||||
return metal_renderer && metal_renderer->IsActive() &&
|
||||
metal_renderer->NeedsSurfaceRedraw();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SlintVideoPresenter::SelectStream(std::string remote_id) {
|
||||
const bool changed = impl_->renderer.SetSelectedStream(remote_id);
|
||||
if (changed && !remote_id.empty()) {
|
||||
impl_->seeded_frame_sequence.erase(remote_id);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
|
||||
const Frame& frame) {
|
||||
PresentResult result;
|
||||
if (!impl_->stream || frame.remote_id.empty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (impl_->renderer.IsActive() && frame.nv12 && frame.width > 0 &&
|
||||
frame.height > 0 && frame.sequence > 0 &&
|
||||
impl_->seeded_frame_sequence[frame.remote_id] != frame.sequence) {
|
||||
const auto submit_result = impl_->renderer.SubmitCachedNv12(
|
||||
frame.remote_id, frame.nv12->data(), frame.nv12->size(), frame.width,
|
||||
frame.height);
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted ||
|
||||
submit_result == VideoRenderer::SubmitResult::dropped) {
|
||||
impl_->seeded_frame_sequence[frame.remote_id] = frame.sequence;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
auto* metal_renderer =
|
||||
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer);
|
||||
if (metal_renderer && metal_renderer->IsActive()) {
|
||||
const SurfaceState surface = impl_->CurrentState();
|
||||
const double top_inset =
|
||||
!surface.fullscreen && surface.tab_count > 1 ? 30.0 : 0.0;
|
||||
const auto outcome = metal_renderer->RenderLatest(
|
||||
frame.remote_id, top_inset, !surface.fullscreen);
|
||||
if (outcome.result == VideoRenderer::RenderResult::rendered) {
|
||||
(*impl_->stream)->set_has_frame(true);
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
result.width = outcome.width;
|
||||
result.height = outcome.height;
|
||||
result.new_frame =
|
||||
impl_->MarkPresented(frame.remote_id, outcome.sequence);
|
||||
} else if (outcome.result == VideoRenderer::RenderResult::empty) {
|
||||
(*impl_->stream)->set_has_frame(false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#else
|
||||
auto* opengl_renderer =
|
||||
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
|
||||
if (opengl_renderer && opengl_renderer->IsReady()) {
|
||||
opengl_renderer->SetSelectedStream(frame.remote_id);
|
||||
if (!(*impl_->stream)->get_native_video_enabled()) {
|
||||
(*impl_->stream)->set_native_video_enabled(true);
|
||||
}
|
||||
|
||||
const bool has_frame =
|
||||
frame.width > 0 && frame.height > 0 && frame.sequence > 0;
|
||||
(*impl_->stream)->set_has_frame(has_frame);
|
||||
if (has_frame) {
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
result.width = frame.width;
|
||||
result.height = frame.height;
|
||||
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||
}
|
||||
(*impl_->stream)->window().request_redraw();
|
||||
return result;
|
||||
}
|
||||
if ((*impl_->stream)->get_native_video_enabled()) {
|
||||
(*impl_->stream)->set_native_video_enabled(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
const size_t nv12_size =
|
||||
static_cast<size_t>(frame.width) * frame.height * 3 / 2;
|
||||
if (!frame.nv12 || frame.width <= 0 || frame.height <= 0 ||
|
||||
frame.nv12->size() < nv12_size) {
|
||||
(*impl_->stream)->set_has_frame(false);
|
||||
return result;
|
||||
}
|
||||
if (impl_->displayed_frame_sequence[frame.remote_id] == frame.sequence) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const auto window_size = (*impl_->stream)->window().size();
|
||||
const VideoRenderSize render_size = FitVideoToRenderArea(
|
||||
frame.width, frame.height, static_cast<int>(window_size.width),
|
||||
static_cast<int>(window_size.height));
|
||||
const uint8_t* y_plane = frame.nv12->data();
|
||||
const uint8_t* uv_plane =
|
||||
frame.nv12->data() + static_cast<size_t>(frame.width) * frame.height;
|
||||
int output_width = frame.width;
|
||||
int output_height = frame.height;
|
||||
|
||||
if (render_size.width > 0 && render_size.height > 0 &&
|
||||
(render_size.width != frame.width ||
|
||||
render_size.height != frame.height)) {
|
||||
const size_t scaled_nv12_size =
|
||||
static_cast<size_t>(render_size.width) * render_size.height * 3 / 2;
|
||||
impl_->scaled_video_frame.resize(scaled_nv12_size);
|
||||
uint8_t* scaled_y = impl_->scaled_video_frame.data();
|
||||
uint8_t* scaled_uv =
|
||||
scaled_y + static_cast<size_t>(render_size.width) * render_size.height;
|
||||
if (ScaleNv12ViaI420(y_plane, frame.width, uv_plane, frame.width,
|
||||
frame.width, frame.height, scaled_y, render_size.width,
|
||||
scaled_uv, render_size.width, render_size.width,
|
||||
render_size.height, libyuv::kFilterBox,
|
||||
&impl_->scaled_video_frame_scratch) == 0) {
|
||||
y_plane = scaled_y;
|
||||
uv_plane = scaled_uv;
|
||||
output_width = render_size.width;
|
||||
output_height = render_size.height;
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
uint32_t texture_id = 0;
|
||||
{
|
||||
std::lock_guard lock(impl_->gl_mutex);
|
||||
texture_id = impl_->gl_texture;
|
||||
}
|
||||
if (texture_id != 0) {
|
||||
impl_->gl_conversion_frame.resize(static_cast<size_t>(output_width) *
|
||||
output_height * 4);
|
||||
if (libyuv::NV12ToABGR(y_plane, output_width, uv_plane, output_width,
|
||||
impl_->gl_conversion_frame.data(), output_width * 4,
|
||||
output_width, output_height) != 0) {
|
||||
return result;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(impl_->gl_mutex);
|
||||
if (impl_->gl_texture == 0) {
|
||||
return result;
|
||||
}
|
||||
texture_id = impl_->gl_texture;
|
||||
impl_->gl_pending_frame.swap(impl_->gl_conversion_frame);
|
||||
impl_->gl_pending_size = {output_width, output_height};
|
||||
impl_->gl_pending_dirty = true;
|
||||
}
|
||||
const VideoRenderSize output_size{output_width, output_height};
|
||||
if (impl_->gl_image_size != output_size) {
|
||||
(*impl_->stream)
|
||||
->set_frame(slint::Image::create_from_borrowed_gl_2d_rgba_texture(
|
||||
texture_id,
|
||||
slint::Size<uint32_t>{static_cast<uint32_t>(output_width),
|
||||
static_cast<uint32_t>(output_height)}));
|
||||
impl_->gl_image_size = output_size;
|
||||
}
|
||||
(*impl_->stream)->set_has_frame(true);
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
(*impl_->stream)->window().request_redraw();
|
||||
result.width = frame.width;
|
||||
result.height = frame.height;
|
||||
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(output_width,
|
||||
output_height);
|
||||
if (libyuv::NV12ToRAW(y_plane, output_width, uv_plane, output_width,
|
||||
reinterpret_cast<uint8_t*>(pixels.begin()),
|
||||
output_width * 3, output_width, output_height) != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
(*impl_->stream)->set_frame(slint::Image(std::move(pixels)));
|
||||
(*impl_->stream)->set_has_frame(true);
|
||||
(*impl_->stream)->set_receiving_text("");
|
||||
result.width = frame.width;
|
||||
result.height = frame.height;
|
||||
result.new_frame = impl_->MarkPresented(frame.remote_id, frame.sequence);
|
||||
return result;
|
||||
}
|
||||
|
||||
void SlintVideoPresenter::ForgetStream(const std::string& remote_id) {
|
||||
impl_->displayed_frame_sequence.erase(remote_id);
|
||||
impl_->seeded_frame_sequence.erase(remote_id);
|
||||
}
|
||||
|
||||
void SlintVideoPresenter::Detach() {
|
||||
impl_->renderer.SetSelectedStream({});
|
||||
#if defined(__APPLE__)
|
||||
if (auto* metal_renderer =
|
||||
dynamic_cast<MacMetalVideoRenderer*>(&impl_->renderer)) {
|
||||
metal_renderer->Detach();
|
||||
}
|
||||
impl_->attachment_attempts = 0;
|
||||
impl_->skip_attachment_once = false;
|
||||
#endif
|
||||
impl_->stream = nullptr;
|
||||
impl_->state_provider = {};
|
||||
impl_->dirty_callback = {};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,75 @@
|
||||
#ifndef CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
#define CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "crossdesk_ui.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class VideoRenderer;
|
||||
|
||||
// Owns the bridge between decoded NV12 frames, the selected native graphics
|
||||
// backend and Slint's StreamWindow. Platform macros and graphics-API lifecycle
|
||||
// details are intentionally contained in the implementation file.
|
||||
class SlintVideoPresenter {
|
||||
public:
|
||||
struct SurfaceState {
|
||||
std::string selected_stream;
|
||||
bool fullscreen = false;
|
||||
size_t tab_count = 0;
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
std::string remote_id;
|
||||
std::shared_ptr<std::vector<unsigned char>> nv12;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint64_t sequence = 0;
|
||||
};
|
||||
|
||||
struct PresentResult {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
bool new_frame = false;
|
||||
};
|
||||
|
||||
using StateProvider = std::function<SurfaceState()>;
|
||||
using DirtyCallback = std::function<void()>;
|
||||
|
||||
explicit SlintVideoPresenter(VideoRenderer& renderer);
|
||||
~SlintVideoPresenter();
|
||||
|
||||
SlintVideoPresenter(const SlintVideoPresenter&) = delete;
|
||||
SlintVideoPresenter& operator=(const SlintVideoPresenter&) = delete;
|
||||
|
||||
// Called before the window is shown. OpenGL installs Slint's rendering
|
||||
// notifier here; macOS deliberately defers native view attachment until a
|
||||
// later UI pass, after AppKit has committed the ordinary window style.
|
||||
void PrepareWindow(slint::ComponentHandle<ui::StreamWindow>& stream,
|
||||
StateProvider state_provider,
|
||||
DirtyCallback dirty_callback);
|
||||
|
||||
// Returns true only when a deferred native surface became attached during
|
||||
// this call, allowing the caller to seed it from a cached frame once.
|
||||
bool EnsureAttached();
|
||||
bool NeedsRedraw() const;
|
||||
|
||||
bool SelectStream(std::string remote_id);
|
||||
PresentResult Present(const Frame& frame);
|
||||
void ForgetStream(const std::string& remote_id);
|
||||
void Detach();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RENDERING_SLINT_VIDEO_PRESENTER_H_
|
||||
@@ -0,0 +1,477 @@
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr auto kPresenceProbeTimeout = std::chrono::seconds(5);
|
||||
} // 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::HandlePresenceProbeTimeout() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleServerControllerDisconnected(
|
||||
const std::string& remote_id, const char* reason) {
|
||||
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
||||
{
|
||||
std::lock_guard lock(remote_pointer_input_mutex_);
|
||||
last_remote_pointer_input_time_.erase(remote_id);
|
||||
}
|
||||
|
||||
bool has_connected_controller = false;
|
||||
bool has_web_controller = false;
|
||||
std::string remaining_controller_id;
|
||||
{
|
||||
std::unique_lock lock(connection_status_mutex_);
|
||||
connection_status_.erase(remote_id);
|
||||
connection_host_names_.erase(remote_id);
|
||||
for (const auto& [id, status] : connection_status_) {
|
||||
if (status != ConnectionStatus::Connected) {
|
||||
continue;
|
||||
}
|
||||
has_connected_controller = true;
|
||||
has_web_controller =
|
||||
has_web_controller || id.find("web") != std::string::npos;
|
||||
if (remaining_controller_id.empty()) {
|
||||
remaining_controller_id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
show_cursor_ = has_web_controller;
|
||||
if (has_connected_controller) {
|
||||
remote_client_id_ = remaining_controller_id;
|
||||
return;
|
||||
}
|
||||
|
||||
need_to_create_server_window_.store(false, std::memory_order_release);
|
||||
need_to_destroy_server_window_.store(true, std::memory_order_release);
|
||||
is_server_mode_ = false;
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
// Keep Wayland capture session warm to avoid black screen on subsequent
|
||||
// reconnects.
|
||||
start_screen_capturer_ = true;
|
||||
LOG_INFO(
|
||||
"Keeping Wayland screen capturer running after disconnect to "
|
||||
"preserve reconnect stability");
|
||||
} else {
|
||||
start_screen_capturer_ = false;
|
||||
}
|
||||
#else
|
||||
start_screen_capturer_ = false;
|
||||
#endif
|
||||
start_speaker_capturer_ = false;
|
||||
start_mouse_controller_ = false;
|
||||
start_keyboard_capturer_ = false;
|
||||
remote_client_id_.clear();
|
||||
if (audio_capture_) {
|
||||
devices_.StopSpeakerCapturer();
|
||||
audio_capture_ = false;
|
||||
}
|
||||
devices_.ResetToInitialDisplay();
|
||||
}
|
||||
|
||||
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) {
|
||||
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()) &&
|
||||
props->thumbnail_frame_ && !props->thumbnail_frame_->empty()) {
|
||||
frame_snapshot = props->thumbnail_frame_;
|
||||
video_width = props->thumbnail_width_;
|
||||
video_height = props->thumbnail_height_;
|
||||
}
|
||||
}
|
||||
auto* native_renderer = video_renderer_.get();
|
||||
if ((!frame_snapshot || frame_snapshot->empty()) && native_renderer) {
|
||||
auto native_snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (native_renderer->CopyLatestNv12(props->remote_id_,
|
||||
native_snapshot.get(), &video_width,
|
||||
&video_height)) {
|
||||
frame_snapshot = std::move(native_snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
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 (native_renderer) {
|
||||
native_renderer->DiscardStream(props->remote_id_);
|
||||
video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
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) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->thumbnail_frame_.reset();
|
||||
props->thumbnail_width_ = 0;
|
||||
props->thumbnail_height_ = 0;
|
||||
props->background_snapshot_time_ = {};
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->remote_cursor_state_mutex_);
|
||||
props->remote_cursor_state_ = {};
|
||||
props->remote_cursor_state_received_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||
const std::string& remote_id) {
|
||||
if (remote_id.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_lock lock(remote_sessions_mutex_);
|
||||
auto it = remote_sessions_.find(remote_id);
|
||||
if (it == remote_sessions_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||
bool remember_password, bool bypass_presence_check) {
|
||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||
int ret =
|
||||
RequestSingleDevicePresence(remote_id, password, remember_password);
|
||||
if (ret != 0) {
|
||||
offline_warning_text_ =
|
||||
localization::device_offline[localization_language_index_];
|
||||
show_offline_warning_window_ = true;
|
||||
LOG_WARN("Presence probe failed for [{}], ret={}", remote_id, ret);
|
||||
} else {
|
||||
LOG_INFO("Presence probe requested for [{}] before connect", remote_id);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
LOG_INFO("Connect to [{}]", remote_id);
|
||||
focused_remote_id_ = remote_id;
|
||||
|
||||
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
||||
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
|
||||
// shared_lock.unlock();
|
||||
|
||||
if (!exists) {
|
||||
PeerPtr* peer_to_init = nullptr;
|
||||
std::string local_id;
|
||||
|
||||
{
|
||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
||||
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
|
||||
auto props = remote_sessions_[remote_id];
|
||||
props->local_id_ = "C-" + std::string(client_id_);
|
||||
props->remote_id_ = remote_id;
|
||||
memcpy(&props->params_, ¶ms_, sizeof(Params));
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 7.0f;
|
||||
|
||||
props->connection_status_.store(ConnectionStatus::Connecting);
|
||||
show_connection_status_window_ = true;
|
||||
|
||||
if (!props->peer_) {
|
||||
LOG_INFO("Create peer [{}] instance failed", props->local_id_);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto& displays = devices_.display_info_list();
|
||||
for (size_t index = 0; index < displays.size(); ++index) {
|
||||
const std::string stream_id = MakeDisplayStreamId(index);
|
||||
AddVideoStream(props->peer_, stream_id.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);
|
||||
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
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef _CURSOR_POSITION_H_
|
||||
#define _CURSOR_POSITION_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "display_info.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline void ResetCursorPosition(CursorState* state) {
|
||||
if (!state) return;
|
||||
state->position_update = true;
|
||||
state->position_valid = false;
|
||||
state->x = 0.5f;
|
||||
state->y = 0.5f;
|
||||
state->visual_offset_x = 0.0f;
|
||||
state->visual_offset_y = 0.0f;
|
||||
state->display_id = -1;
|
||||
}
|
||||
|
||||
inline bool NormalizeCursorPosition(
|
||||
double screen_x, double screen_y,
|
||||
const std::vector<DisplayInfo>& displays, int preferred_display,
|
||||
CursorState* state) {
|
||||
if (!state) return false;
|
||||
ResetCursorPosition(state);
|
||||
|
||||
auto contains = [&](int index) {
|
||||
if (index < 0 || index >= static_cast<int>(displays.size())) return false;
|
||||
const auto& display = displays[index];
|
||||
return display.width > 0 && display.height > 0 &&
|
||||
screen_x >= display.left && screen_x < display.right &&
|
||||
screen_y >= display.top && screen_y < display.bottom;
|
||||
};
|
||||
|
||||
int display_id = contains(preferred_display) ? preferred_display : -1;
|
||||
if (display_id < 0) {
|
||||
for (int index = 0; index < static_cast<int>(displays.size()); ++index) {
|
||||
if (contains(index)) {
|
||||
display_id = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (display_id < 0) return false;
|
||||
|
||||
const auto& display = displays[display_id];
|
||||
// Screen coordinates describe a continuous rectangle with an exclusive
|
||||
// right/bottom edge. Use its full extent so feedback is the exact inverse of
|
||||
// normalized input instead of accumulating a one-pixel edge convention.
|
||||
const double horizontal_extent = std::max(display.width, 1);
|
||||
const double vertical_extent = std::max(display.height, 1);
|
||||
state->position_valid = true;
|
||||
state->x = static_cast<float>(std::clamp(
|
||||
(screen_x - display.left) / horizontal_extent, 0.0, 1.0));
|
||||
state->y = static_cast<float>(std::clamp(
|
||||
(screen_y - display.top) / vertical_extent, 0.0, 1.0));
|
||||
state->display_id = display_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "runtime/cursor_state_provider.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "runtime/cursor_position.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
|
||||
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
|
||||
}
|
||||
|
||||
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
|
||||
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
|
||||
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
|
||||
if (IsSystemCursor(cursor, IDC_APPSTARTING))
|
||||
return RemoteCursorShape::progress;
|
||||
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
|
||||
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
|
||||
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
|
||||
if (IsSystemCursor(cursor, IDC_NO))
|
||||
return RemoteCursorShape::not_allowed;
|
||||
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
|
||||
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
|
||||
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
|
||||
if (IsSystemCursor(cursor, IDC_SIZENESW))
|
||||
return RemoteCursorShape::nesw_resize;
|
||||
if (IsSystemCursor(cursor, IDC_SIZENWSE))
|
||||
return RemoteCursorShape::nwse_resize;
|
||||
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
|
||||
return RemoteCursorShape::default_cursor;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct CursorStateProvider::Impl {};
|
||||
|
||||
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||
CursorStateProvider::~CursorStateProvider() = default;
|
||||
|
||||
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state) {
|
||||
if (!state) return false;
|
||||
|
||||
CURSORINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
if (!GetCursorInfo(&info)) return false;
|
||||
|
||||
state->seq = 0;
|
||||
state->visible = (info.flags & CURSOR_SHOWING) != 0;
|
||||
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
||||
: RemoteCursorShape::none;
|
||||
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
|
||||
preferred_display, state);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#elif defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "linux_cursor_shape.h"
|
||||
#include "platform.h"
|
||||
#include "shared_cursor_state.h"
|
||||
#include "runtime/cursor_position.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
bool CursorHasVisiblePixel(const XFixesCursorImage& image) {
|
||||
const size_t pixel_count =
|
||||
static_cast<size_t>(image.width) * static_cast<size_t>(image.height);
|
||||
for (size_t index = 0; index < pixel_count; ++index) {
|
||||
if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct CursorStateProvider::Impl {
|
||||
Display* display = XOpenDisplay(nullptr);
|
||||
|
||||
~Impl() {
|
||||
if (display) XCloseDisplay(display);
|
||||
}
|
||||
};
|
||||
|
||||
CursorStateProvider::CursorStateProvider() : impl_(std::make_unique<Impl>()) {}
|
||||
CursorStateProvider::~CursorStateProvider() = default;
|
||||
|
||||
bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state) {
|
||||
if (!state || !impl_) return false;
|
||||
ResetCursorPosition(state);
|
||||
|
||||
if (IsWaylandSession()) {
|
||||
SharedCursorState shared{};
|
||||
if (GetSharedCursorState(&shared)) {
|
||||
state->seq = 0;
|
||||
state->visible = shared.visible;
|
||||
state->shape = shared.visible ? shared.shape : RemoteCursorShape::none;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!impl_->display) return false;
|
||||
|
||||
XFixesCursorImage* image = XFixesGetCursorImage(impl_->display);
|
||||
if (!image) return false;
|
||||
|
||||
const std::string name = image->name ? image->name : "";
|
||||
|
||||
state->seq = 0;
|
||||
state->visible = CursorHasVisiblePixel(*image);
|
||||
state->shape = state->visible ? ShapeFromLinuxCursorName(name)
|
||||
: RemoteCursorShape::none;
|
||||
NormalizeCursorPosition(image->x, image->y, displays, preferred_display,
|
||||
state);
|
||||
XFree(image);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef _CURSOR_STATE_PROVIDER_H_
|
||||
#define _CURSOR_STATE_PROVIDER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include "display_info.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Samples the cursor that is currently displayed by the controlled desktop
|
||||
// and converts platform-specific cursor handles to protocol cursor shapes.
|
||||
class CursorStateProvider {
|
||||
public:
|
||||
CursorStateProvider();
|
||||
~CursorStateProvider();
|
||||
|
||||
CursorStateProvider(const CursorStateProvider&) = delete;
|
||||
CursorStateProvider& operator=(const CursorStateProvider&) = delete;
|
||||
|
||||
bool Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-02-28
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
#define CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class DevicePresenceCache {
|
||||
public:
|
||||
void SetOnline(const std::string &device_id, bool online) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
cache_[device_id] = online;
|
||||
}
|
||||
|
||||
bool IsOnline(const std::string &device_id) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
const auto it = cache_.find(device_id);
|
||||
return it != cache_.end() && it->second;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
cache_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, bool> cache_;
|
||||
mutable std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_DEVICE_PRESENCE_CACHE_H_
|
||||
@@ -0,0 +1,300 @@
|
||||
#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 <display_stream_id.h>
|
||||
#include "localization.h"
|
||||
#include "platform/video_renderer.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();
|
||||
settings_.ActivateCachedPublicIdentity();
|
||||
params_.user_id = client_id_with_password_;
|
||||
}
|
||||
|
||||
const bool self_hosted = config_center_->IsSelfHosted();
|
||||
const std::string pending_identity =
|
||||
settings_.PendingPasswordChangeIdentity(
|
||||
self_hosted, signal_server_ip, signal_server_port);
|
||||
bool try_pending_identity = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(password_change_mutex_);
|
||||
if (pending_identity.empty()) {
|
||||
credential_recovery_in_progress_ = false;
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
credential_recovery_retry_active_ = false;
|
||||
credential_recovery_promote_pending_ = false;
|
||||
credential_recovery_clear_pending_ = false;
|
||||
} else if (!credential_recovery_in_progress_) {
|
||||
credential_recovery_in_progress_ = true;
|
||||
credential_recovery_attempt_pending_ = true;
|
||||
LOG_INFO("Recovering an interrupted password change for [{}]",
|
||||
client_id_);
|
||||
}
|
||||
try_pending_identity = credential_recovery_in_progress_ &&
|
||||
credential_recovery_attempt_pending_ &&
|
||||
!pending_identity.empty();
|
||||
}
|
||||
|
||||
const char *active_identity =
|
||||
self_hosted ? self_hosted_user_id_ : client_id_with_password_;
|
||||
const std::string login_identity =
|
||||
try_pending_identity ? pending_identity : std::string(active_identity);
|
||||
std::memset(connection_login_identity_, 0,
|
||||
sizeof(connection_login_identity_));
|
||||
std::strncpy(connection_login_identity_, login_identity.c_str(),
|
||||
sizeof(connection_login_identity_) - 1);
|
||||
params_.user_id = connection_login_identity_;
|
||||
|
||||
// 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;
|
||||
// TURN credentials are issued by the signaling server after login. Keep the
|
||||
// initial values empty so a reusable static password is never embedded in
|
||||
// the client binary.
|
||||
params_.turn_server_username[0] = '\0';
|
||||
params_.turn_server_password[0] = '\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();
|
||||
// The Slint desktop renderer currently consumes packed CPU frames. Native
|
||||
// renderers can opt into platform-native frame output independently.
|
||||
params_.native_video_output = false;
|
||||
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_content_type = VideoContentType::ScreenContent;
|
||||
params_.video_quality =
|
||||
static_cast<VideoQuality>(config_center_->GetVideoQuality());
|
||||
params_.video_frame_rate =
|
||||
config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
: 60;
|
||||
params_.video_degradation_preference =
|
||||
VideoDegradationPreference::MaintainFrameRate;
|
||||
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_;
|
||||
|
||||
// The previous peer may have left a terminal status behind. Reset it before
|
||||
// Init() starts emitting callbacks for the newly selected server.
|
||||
signal_connected_ = false;
|
||||
signal_status_ = SignalStatus::SignalConnecting;
|
||||
|
||||
peer_ = CreatePeer(¶ms_);
|
||||
if (peer_) {
|
||||
LOG_INFO("Create peer instance [{}] successful", client_id_);
|
||||
Init(peer_);
|
||||
LOG_INFO("Peer [{}] init finish", client_id_);
|
||||
} else {
|
||||
LOG_INFO("Create peer [{}] instance failed", client_id_);
|
||||
}
|
||||
|
||||
if (0 == devices_.InitializeScreenCapturer()) {
|
||||
const auto &displays = devices_.display_info_list();
|
||||
for (size_t index = 0; index < displays.size(); ++index) {
|
||||
const std::string stream_id = MakeDisplayStreamId(index);
|
||||
AddVideoStream(peer_, stream_id.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_ && thumbnail_) {
|
||||
uint32_t now_time = SDL_GetTicks();
|
||||
if (now_time - recent_connection_image_save_time_ >= 50) {
|
||||
int ret = thumbnail_->LoadThumbnail(recent_connections_,
|
||||
&recent_connection_image_width_,
|
||||
&recent_connection_image_height_);
|
||||
if (!ret) {
|
||||
LOG_INFO("Load recent connection thumbnails");
|
||||
}
|
||||
reload_recent_connections_ = false;
|
||||
|
||||
recent_connection_ids_.clear();
|
||||
for (const auto &conn : recent_connections_) {
|
||||
recent_connection_ids_.push_back(conn.first);
|
||||
}
|
||||
need_to_send_recent_connections_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void GuiRuntime::SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len) {
|
||||
GuiRuntime *runtime = static_cast<GuiRuntime *>(userdata);
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (1) {
|
||||
std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
for (const auto &it : runtime->remote_sessions_) {
|
||||
auto props = it.second;
|
||||
if (props->connection_status_.load() == ConnectionStatus::Connected) {
|
||||
if (props->peer_) {
|
||||
SendAudioFrame(props->peer_, (const char *)stream, len,
|
||||
runtime->audio_label_.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
memcpy(runtime->audio_buffer_, stream, len);
|
||||
runtime->audio_len_ = len;
|
||||
SDL_Delay(10);
|
||||
runtime->audio_buffer_fresh_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::SdlCaptureAudioOut([[maybe_unused]] void *userdata,
|
||||
[[maybe_unused]] Uint8 *stream,
|
||||
[[maybe_unused]] int len) {
|
||||
// GuiApplication *runtime = (GuiApplication *)userdata;
|
||||
// for (auto it : runtime->remote_sessions_) {
|
||||
// auto props = it.second;
|
||||
// if (props->connection_status_ == SignalStatus::SignalConnected) {
|
||||
// SendAudioFrame(props->peer_, (const char *)stream, len);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (!runtime->audio_buffer_fresh_) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// SDL_memset(stream, 0, len);
|
||||
|
||||
// if (runtime->audio_len_ == 0) {
|
||||
// return;
|
||||
// } else {
|
||||
// }
|
||||
|
||||
// len = (len > runtime->audio_len_ ? runtime->audio_len_ : len);
|
||||
// SDL_MixAudioFormat(stream, runtime->audio_buffer_, AUDIO_S16LSB, len,
|
||||
// SDL_MIX_MAXVOLUME);
|
||||
// runtime->audio_buffer_fresh_ = false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifndef CROSSDESK_GUI_RUNTIME_H_
|
||||
#define CROSSDESK_GUI_RUNTIME_H_
|
||||
|
||||
#include <atomic>
|
||||
#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 "features/settings/settings_manager.h"
|
||||
#include "runtime/gui_state.h"
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class VideoRenderer;
|
||||
|
||||
// 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 HandlePresenceProbeTimeout();
|
||||
void HandleServerControllerDisconnected(const std::string& remote_id,
|
||||
const char* reason);
|
||||
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_;
|
||||
std::atomic<bool> video_frame_dirty_{false};
|
||||
std::unique_ptr<VideoRenderer> video_renderer_;
|
||||
|
||||
private:
|
||||
friend class ClipboardController;
|
||||
friend class SessionDeviceManager;
|
||||
friend class FileTransferManager;
|
||||
friend class SettingsManager;
|
||||
friend class KeyboardController;
|
||||
friend class PeerEventHandler;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Aggregate state visible to GuiRuntime and GuiApplication.
|
||||
*
|
||||
* Definitions live beside their owners: SDL/window state in application and
|
||||
* connection/session state in runtime. This header is intentionally only the
|
||||
* composition point.
|
||||
*/
|
||||
|
||||
#ifndef CROSSDESK_GUI_STATE_H_
|
||||
#define CROSSDESK_GUI_STATE_H_
|
||||
|
||||
#include "application/application_state.h"
|
||||
#include "runtime/runtime_state.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct GuiState : ApplicationState, RuntimeState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif // CROSSDESK_GUI_STATE_H_
|
||||
@@ -0,0 +1,319 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <remote_action.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 "filesystem_utf8.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(PathFromUtf8(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 {
|
||||
const auto encoded_ack = EncodeFileTransferAck(ack);
|
||||
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, encoded_ack.data(), encoded_ack.size(),
|
||||
runtime->file_feedback_label_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return SendReliableDataFrame(
|
||||
runtime->peer_, encoded_ack.data(), encoded_ack.size(),
|
||||
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::cursor_state) {
|
||||
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()) {
|
||||
props = props_it->second;
|
||||
}
|
||||
}
|
||||
if (!props) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||
const uint32_t previous_seq = props->remote_cursor_state_.seq;
|
||||
const bool is_newer =
|
||||
!props->remote_cursor_state_received_ ||
|
||||
static_cast<int32_t>(remote_action.cs.seq - previous_seq) > 0;
|
||||
if (is_newer) {
|
||||
const bool changed =
|
||||
!props->remote_cursor_state_received_ ||
|
||||
props->remote_cursor_state_.visible != remote_action.cs.visible ||
|
||||
props->remote_cursor_state_.shape != remote_action.cs.shape;
|
||||
CursorState merged = remote_action.cs;
|
||||
if (!remote_action.cs.position_update &&
|
||||
props->remote_cursor_state_received_) {
|
||||
merged.position_valid = props->remote_cursor_state_.position_valid;
|
||||
merged.x = props->remote_cursor_state_.x;
|
||||
merged.y = props->remote_cursor_state_.y;
|
||||
merged.display_id = props->remote_cursor_state_.display_id;
|
||||
}
|
||||
props->remote_cursor_state_ = merged;
|
||||
props->remote_cursor_state_received_ = true;
|
||||
if (changed) {
|
||||
LOG_INFO("Received cursor state: seq={}, visible={}, shape={}",
|
||||
remote_action.cs.seq, remote_action.cs.visible,
|
||||
static_cast<int>(remote_action.cs.shape));
|
||||
}
|
||||
}
|
||||
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 (runtime->is_server_mode_ &&
|
||||
remote_action.type == ControlType::mouse) {
|
||||
std::lock_guard lock(runtime->remote_pointer_input_mutex_);
|
||||
runtime->last_remote_pointer_input_time_[remote_id] =
|
||||
std::chrono::steady_clock::now();
|
||||
}
|
||||
#if _WIN32
|
||||
if (runtime->local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
|
||||
remote_action.type != ControlType::keyboard &&
|
||||
remote_action.type != ControlType::keyboard_state) {
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
int absolute_x = 0;
|
||||
int absolute_y = 0;
|
||||
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
|
||||
runtime->selected_display_,
|
||||
remote_action.m.x, remote_action.m.y,
|
||||
&absolute_x, &absolute_y)) {
|
||||
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
|
||||
"mapping: display_index={}, x={}, y={}",
|
||||
runtime->selected_display_, remote_action.m.x,
|
||||
remote_action.m.y);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string response = SendCrossDeskSecureDesktopMouseInput(
|
||||
absolute_x, absolute_y, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
LogSecureDesktopInputBlocked(
|
||||
&runtime->last_local_secure_input_block_log_tick_, "local",
|
||||
runtime->local_interactive_stage_.c_str());
|
||||
LOG_WARN(
|
||||
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
|
||||
"flag={}, response={}",
|
||||
absolute_x, absolute_y, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
runtime->devices_.SendMouseCommand(remote_action,
|
||||
runtime->selected_display_);
|
||||
} else if (remote_action.type == ControlType::audio_capture) {
|
||||
if (remote_action.a && !runtime->start_speaker_capturer_)
|
||||
runtime->devices_.StartSpeakerCapturer();
|
||||
else if (!remote_action.a && runtime->start_speaker_capturer_)
|
||||
runtime->devices_.StopSpeakerCapturer();
|
||||
} else if (remote_action.type == ControlType::keyboard) {
|
||||
runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action);
|
||||
} else if (remote_action.type == ControlType::keyboard_state) {
|
||||
runtime->keyboard_.ApplyRemoteState(remote_id, remote_action);
|
||||
} else if (remote_action.type == ControlType::display_id) {
|
||||
const int ret = runtime->devices_.SwitchDisplay(remote_action.d);
|
||||
if (ret == 0) {
|
||||
runtime->selected_display_ = remote_action.d;
|
||||
} else {
|
||||
LOG_WARN("Display switch skipped, invalid display_id={}",
|
||||
remote_action.d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,579 @@
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
#include <remote_action.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 <vector>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.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::SendClientInfo(PeerPtr* peer,
|
||||
const std::string& client_id) {
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
constexpr const char* kClientPlatform = "windows";
|
||||
#elif defined(__APPLE__)
|
||||
constexpr const char* kClientPlatform = "macos";
|
||||
#elif defined(__linux__)
|
||||
constexpr const char* kClientPlatform = "linux";
|
||||
#else
|
||||
constexpr const char* kClientPlatform = "unknown";
|
||||
#endif
|
||||
|
||||
const nlohmann::json message = {{"type", "client_info"},
|
||||
{"version", CROSSDESK_VERSION},
|
||||
{"platform", kClientPlatform}};
|
||||
const std::string payload = message.dump();
|
||||
if (SendSignalMessage(peer, payload.data(), payload.size()) != 0) {
|
||||
LOG_WARN("[{}] failed to report client information", client_id);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (type == "change_password") {
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (!runtime->password_change_pending_) {
|
||||
LOG_WARN("Ignore unexpected password change response");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!j.contains("request_id") || !j["request_id"].is_string() ||
|
||||
j["request_id"].get<std::string>() !=
|
||||
runtime->pending_password_change_request_id_) {
|
||||
LOG_WARN("Ignore password change response with unexpected request id");
|
||||
return;
|
||||
}
|
||||
|
||||
if (j.contains("user_id") && j["user_id"].is_string()) {
|
||||
const std::string response_user_id = j["user_id"].get<std::string>();
|
||||
if (!response_user_id.empty() &&
|
||||
response_user_id != runtime->client_id_) {
|
||||
LOG_WARN("Ignore password change response for unexpected id [{}]",
|
||||
response_user_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
runtime->password_change_succeeded_ =
|
||||
j.contains("status") && j["status"].is_string() &&
|
||||
j["status"].get<std::string>() == "success";
|
||||
runtime->password_change_error_ =
|
||||
j.contains("reason") && j["reason"].is_string()
|
||||
? j["reason"].get<std::string>()
|
||||
: "Password change failed";
|
||||
runtime->password_change_result_uncertain_ = false;
|
||||
runtime->password_change_result_ready_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
SendClientInfo(runtime->peer_, client_id);
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (runtime->credential_recovery_in_progress_) {
|
||||
if (runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_promote_pending_ = true;
|
||||
} else {
|
||||
runtime->credential_recovery_clear_pending_ = true;
|
||||
}
|
||||
}
|
||||
} else if (SignalStatus::SignalFailed == status) {
|
||||
runtime->signal_connected_ = false;
|
||||
std::lock_guard<std::mutex> lock(runtime->password_change_mutex_);
|
||||
if (runtime->credential_recovery_in_progress_ &&
|
||||
runtime->credential_recovery_attempt_pending_) {
|
||||
runtime->credential_recovery_retry_active_ = true;
|
||||
}
|
||||
} 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);
|
||||
SendClientInfo(props->peer_, client_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);
|
||||
|
||||
switch (status) {
|
||||
case ConnectionStatus::Connected: {
|
||||
runtime->ResetRemoteServiceStatus(*props);
|
||||
{
|
||||
std::lock_guard lock(props->remote_cursor_state_mutex_);
|
||||
props->remote_cursor_state_ = {};
|
||||
props->remote_cursor_state_received_ = false;
|
||||
}
|
||||
{
|
||||
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;
|
||||
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 lock(props->remote_cursor_state_mutex_);
|
||||
props->remote_cursor_state_ = {};
|
||||
props->remote_cursor_state_received_ = false;
|
||||
}
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> native_snapshot;
|
||||
int native_snapshot_width = 0;
|
||||
int native_snapshot_height = 0;
|
||||
bool needs_native_snapshot = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
needs_native_snapshot =
|
||||
!props->thumbnail_frame_ || props->thumbnail_frame_->empty();
|
||||
}
|
||||
if (needs_native_snapshot) {
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
auto snapshot = std::make_shared<std::vector<unsigned char>>();
|
||||
if (native_renderer &&
|
||||
native_renderer->CopyLatestNv12(remote_id, snapshot.get(),
|
||||
&native_snapshot_width,
|
||||
&native_snapshot_height)) {
|
||||
native_snapshot = std::move(snapshot);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
if (native_snapshot &&
|
||||
(!props->thumbnail_frame_ || props->thumbnail_frame_->empty())) {
|
||||
props->thumbnail_frame_ = std::move(native_snapshot);
|
||||
props->thumbnail_width_ = native_snapshot_width;
|
||||
props->thumbnail_height_ = native_snapshot_height;
|
||||
}
|
||||
props->video_width_ = 0;
|
||||
props->video_height_ = 0;
|
||||
props->video_size_ = 0;
|
||||
props->render_rect_dirty_ = true;
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer) {
|
||||
native_renderer->DiscardStream(remote_id);
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
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_destroy_server_window_.store(
|
||||
false, std::memory_order_release);
|
||||
runtime->need_to_create_server_window_.store(true,
|
||||
std::memory_order_release);
|
||||
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->HandleServerControllerDisconnected(remote_id,
|
||||
"connection_closed");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnNetStatusReport(
|
||||
const char* client_id, size_t client_id_size, TraversalMode mode,
|
||||
const XNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||
const size_t user_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
||||
std::string id, password;
|
||||
const char* at_pos = strchr(client_id, '@');
|
||||
if (at_pos == nullptr) {
|
||||
id = client_id;
|
||||
password.clear();
|
||||
} else {
|
||||
id.assign(client_id, at_pos - client_id);
|
||||
password = at_pos + 1;
|
||||
}
|
||||
|
||||
bool is_self_hosted = runtime->config_center_->IsSelfHosted();
|
||||
|
||||
if (is_self_hosted) {
|
||||
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
|
||||
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
|
||||
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
|
||||
strncpy(runtime->password_saved_, password.c_str(),
|
||||
sizeof(runtime->password_saved_) - 1);
|
||||
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->self_hosted_id_, 0, sizeof(runtime->self_hosted_id_));
|
||||
strncpy(runtime->self_hosted_id_, client_id,
|
||||
sizeof(runtime->self_hosted_id_) - 1);
|
||||
runtime->self_hosted_id_[sizeof(runtime->self_hosted_id_) - 1] = '\0';
|
||||
|
||||
LOG_INFO("Use self-hosted client id [{}] and save to cache file", id);
|
||||
|
||||
runtime->settings_.PersistSelfHostedIdentity(client_id);
|
||||
} else {
|
||||
memset(&runtime->client_id_, 0, sizeof(runtime->client_id_));
|
||||
strncpy(runtime->client_id_, id.c_str(), sizeof(runtime->client_id_) - 1);
|
||||
runtime->client_id_[sizeof(runtime->client_id_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->password_saved_, 0, sizeof(runtime->password_saved_));
|
||||
strncpy(runtime->password_saved_, password.c_str(),
|
||||
sizeof(runtime->password_saved_) - 1);
|
||||
runtime->password_saved_[sizeof(runtime->password_saved_) - 1] = '\0';
|
||||
|
||||
memset(&runtime->client_id_with_password_, 0,
|
||||
sizeof(runtime->client_id_with_password_));
|
||||
strncpy(runtime->client_id_with_password_, client_id,
|
||||
sizeof(runtime->client_id_with_password_) - 1);
|
||||
runtime
|
||||
->client_id_with_password_[sizeof(runtime->client_id_with_password_) -
|
||||
1] = '\0';
|
||||
|
||||
LOG_INFO("Use client id [{}] and save id into cache file", id);
|
||||
runtime->settings_.Save();
|
||||
}
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
// std::shared_lock lock(runtime->remote_sessions_mutex_);
|
||||
if (runtime->remote_sessions_.find(remote_id) ==
|
||||
runtime->remote_sessions_.end()) {
|
||||
return;
|
||||
}
|
||||
auto props = runtime->remote_sessions_.find(remote_id)->second;
|
||||
if (props->traversal_mode_ != mode) {
|
||||
props->traversal_mode_ = mode;
|
||||
LOG_INFO("Net mode: [{}]", int(props->traversal_mode_));
|
||||
}
|
||||
|
||||
if (!net_traffic_stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only display client side net status if connected to itself
|
||||
if (!(runtime->peer_reserved_ && !strstr(client_id, "C-"))) {
|
||||
props->net_traffic_stats_ = *net_traffic_stats;
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,50 @@
|
||||
#ifndef CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
#define CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#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:
|
||||
static void SendClientInfo(PeerPtr *peer, const std::string &client_id);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_GUI_PEER_EVENT_HANDLER_H_
|
||||
@@ -0,0 +1,145 @@
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "platform/video_renderer.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "runtime/peer_event_handler.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr auto kBackgroundSnapshotInterval = std::chrono::seconds(1);
|
||||
|
||||
} // namespace
|
||||
|
||||
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_) {
|
||||
bool background_snapshot_only = false;
|
||||
auto* native_renderer = runtime->video_renderer_.get();
|
||||
if (native_renderer && native_renderer->IsActive()) {
|
||||
const auto submit_result = native_renderer->SubmitNv12(
|
||||
remote_id, reinterpret_cast<const uint8_t*>(video_frame->data),
|
||||
video_frame->size, video_frame->width, video_frame->height);
|
||||
if (submit_result == VideoRenderer::SubmitResult::submitted) {
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
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;
|
||||
// Once the native renderer owns the current stream, do not leave an
|
||||
// older CPU frame ahead of the renderer's close snapshot.
|
||||
props->front_frame_.reset();
|
||||
props->back_frame_.reset();
|
||||
props->thumbnail_frame_.reset();
|
||||
props->thumbnail_width_ = 0;
|
||||
props->thumbnail_height_ = 0;
|
||||
props->background_snapshot_time_ = {};
|
||||
++props->video_frame_sequence_;
|
||||
props->streaming_ = true;
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
if (submit_result == VideoRenderer::SubmitResult::dropped ||
|
||||
submit_result == VideoRenderer::SubmitResult::failed) {
|
||||
// Keep presenting the last native frame. A later decoded frame can
|
||||
// reuse the renderer without changing ownership mid-window.
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
if (submit_result == VideoRenderer::SubmitResult::not_selected) {
|
||||
background_snapshot_only = true;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (background_snapshot_only && props->thumbnail_frame_ &&
|
||||
!props->thumbnail_frame_->empty() &&
|
||||
props->background_snapshot_time_ !=
|
||||
std::chrono::steady_clock::time_point{} &&
|
||||
now - props->background_snapshot_time_ <
|
||||
kBackgroundSnapshotInterval) {
|
||||
props->streaming_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate a third buffer only while the UI still owns the old snapshot.
|
||||
if (!props->back_frame_ || props->back_frame_.use_count() != 1) {
|
||||
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_);
|
||||
props->thumbnail_frame_ = props->front_frame_;
|
||||
props->thumbnail_width_ = video_frame->width;
|
||||
props->thumbnail_height_ = video_frame->height;
|
||||
if (background_snapshot_only) {
|
||||
props->background_snapshot_time_ = now;
|
||||
}
|
||||
++props->video_frame_sequence_;
|
||||
}
|
||||
|
||||
props->streaming_ = true;
|
||||
if (background_snapshot_only) {
|
||||
return;
|
||||
}
|
||||
runtime->video_frame_dirty_.store(true, std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
void PeerEventHandler::OnReceiveAudioBuffer(
|
||||
const char* data, size_t size, const char* user_id, size_t user_id_size,
|
||||
const char* src_id, size_t src_id_size, void* user_data) {
|
||||
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||
if (!runtime) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtime->audio_buffer_fresh_ = true;
|
||||
|
||||
runtime->devices_.PushAudio(data, size);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "runtime/remote_action_codec.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace crossdesk::remote_action_codec {
|
||||
|
||||
std::vector<char> Serialize(const RemoteAction &action) {
|
||||
std::vector<char> buffer;
|
||||
buffer.push_back(static_cast<char>(action.type));
|
||||
|
||||
auto insert_bytes = [&](const void *ptr, size_t len) {
|
||||
buffer.insert(buffer.end(), static_cast<const char *>(ptr),
|
||||
static_cast<const char *>(ptr) + len);
|
||||
};
|
||||
|
||||
if (action.type == ControlType::host_infomation) {
|
||||
insert_bytes(&action.i.host_name_size, sizeof(size_t));
|
||||
insert_bytes(action.i.host_name, action.i.host_name_size);
|
||||
|
||||
size_t num = action.i.display_num;
|
||||
insert_bytes(&num, sizeof(size_t));
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
const size_t len = std::strlen(action.i.display_list[i]);
|
||||
insert_bytes(&len, sizeof(size_t));
|
||||
insert_bytes(action.i.display_list[i], len);
|
||||
}
|
||||
|
||||
insert_bytes(action.i.left, sizeof(int) * num);
|
||||
insert_bytes(action.i.top, sizeof(int) * num);
|
||||
insert_bytes(action.i.right, sizeof(int) * num);
|
||||
insert_bytes(action.i.bottom, sizeof(int) * num);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool Deserialize(const char *data, size_t size, RemoteAction &out) {
|
||||
size_t offset = 0;
|
||||
auto read = [&](void *dst, size_t len) -> bool {
|
||||
if (offset + len > size) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(dst, data + offset, len);
|
||||
offset += len;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (size < 1) {
|
||||
return false;
|
||||
}
|
||||
out.type = static_cast<ControlType>(data[offset++]);
|
||||
|
||||
if (out.type == ControlType::host_infomation) {
|
||||
size_t name_len;
|
||||
if (!read(&name_len, sizeof(size_t)) ||
|
||||
name_len >= sizeof(out.i.host_name)) {
|
||||
return false;
|
||||
}
|
||||
if (!read(out.i.host_name, name_len)) {
|
||||
return false;
|
||||
}
|
||||
out.i.host_name[name_len] = '\0';
|
||||
out.i.host_name_size = name_len;
|
||||
|
||||
size_t num;
|
||||
if (!read(&num, sizeof(size_t))) {
|
||||
return false;
|
||||
}
|
||||
out.i.display_num = num;
|
||||
|
||||
out.i.display_list =
|
||||
static_cast<char **>(std::malloc(num * sizeof(char *)));
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
size_t len;
|
||||
if (!read(&len, sizeof(size_t)) || offset + len > size) {
|
||||
return false;
|
||||
}
|
||||
out.i.display_list[i] = static_cast<char *>(std::malloc(len + 1));
|
||||
std::memcpy(out.i.display_list[i], data + offset, len);
|
||||
out.i.display_list[i][len] = '\0';
|
||||
offset += len;
|
||||
}
|
||||
|
||||
auto alloc_int_array = [&](int *&array) {
|
||||
array = static_cast<int *>(std::malloc(num * sizeof(int)));
|
||||
return read(array, num * sizeof(int));
|
||||
};
|
||||
|
||||
return alloc_int_array(out.i.left) && alloc_int_array(out.i.top) &&
|
||||
alloc_int_array(out.i.right) && alloc_int_array(out.i.bottom);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Free(RemoteAction &action) {
|
||||
if (action.type != ControlType::host_infomation) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < action.i.display_num; ++i) {
|
||||
std::free(action.i.display_list[i]);
|
||||
}
|
||||
std::free(action.i.display_list);
|
||||
std::free(action.i.left);
|
||||
std::free(action.i.top);
|
||||
std::free(action.i.right);
|
||||
std::free(action.i.bottom);
|
||||
|
||||
action.i.display_list = nullptr;
|
||||
action.i.left = action.i.top = action.i.right = action.i.bottom = nullptr;
|
||||
action.i.display_num = 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk::remote_action_codec
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
#define CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
namespace crossdesk::remote_action_codec {
|
||||
|
||||
std::vector<char> Serialize(const RemoteAction &action);
|
||||
bool Deserialize(const char *data, size_t size, RemoteAction &out);
|
||||
void Free(RemoteAction &action);
|
||||
|
||||
} // namespace crossdesk::remote_action_codec
|
||||
|
||||
#endif // CROSSDESK_GUI_REMOTE_ACTION_CODEC_H_
|
||||
@@ -0,0 +1,177 @@
|
||||
#ifndef _REMOTE_SESSION_H_
|
||||
#define _REMOTE_SESSION_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <remote_action.h>
|
||||
#include <stream_names.h>
|
||||
#include "display_info.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_ = kAudioStream;
|
||||
std::string data_label_ = kDataStream;
|
||||
std::string mouse_label_ = kMouseStream;
|
||||
std::string keyboard_label_ = kKeyboardStream;
|
||||
std::string file_label_ = kFileStream;
|
||||
std::string control_data_label_ = kControlStream;
|
||||
std::string file_feedback_label_ = kFileFeedbackStream;
|
||||
std::string clipboard_label_ = kClipboardStream;
|
||||
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;
|
||||
|
||||
// Written by the decode callback thread and consumed by the UI thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
// Retains the most recent background CPU frame across disconnect cleanup
|
||||
// so closing an unselected tab can still update its recent-item thumbnail.
|
||||
std::shared_ptr<std::vector<unsigned char>> thumbnail_frame_;
|
||||
int thumbnail_width_ = 0;
|
||||
int thumbnail_height_ = 0;
|
||||
std::chrono::steady_clock::time_point background_snapshot_time_;
|
||||
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;
|
||||
uint64_t video_frame_sequence_ = 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_;
|
||||
// Cursor snapshots arrive on the transport callback thread and are applied
|
||||
// by the Slint UI thread. Keep the snapshot atomic as a unit so visibility
|
||||
// and shape cannot briefly come from different protocol messages.
|
||||
std::mutex remote_cursor_state_mutex_;
|
||||
CursorState remote_cursor_state_{};
|
||||
bool remote_cursor_state_received_ = false;
|
||||
// Shared by minirtc callbacks, Slint 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
|
||||
@@ -0,0 +1,213 @@
|
||||
#ifndef _RUNTIME_STATE_H_
|
||||
#define _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 <stream_names.h>
|
||||
|
||||
#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_ = kDataStream;
|
||||
std::string mouse_label_ = kMouseStream;
|
||||
std::string keyboard_label_ = kKeyboardStream;
|
||||
std::string info_label_ = "info";
|
||||
std::string control_data_label_ = kControlStream;
|
||||
std::string file_label_ = kFileStream;
|
||||
std::string file_feedback_label_ = kFileFeedbackStream;
|
||||
std::string clipboard_label_ = kClipboardStream;
|
||||
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 wire contract.
|
||||
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;
|
||||
bool portable_service_installed_ = 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] = "";
|
||||
char connection_login_identity_[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;
|
||||
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 PasswordChangeState {
|
||||
std::mutex password_change_mutex_;
|
||||
uint64_t next_password_change_request_id_ = 0;
|
||||
bool password_change_pending_ = false;
|
||||
bool password_change_result_ready_ = false;
|
||||
bool password_change_succeeded_ = false;
|
||||
bool password_change_result_uncertain_ = false;
|
||||
std::chrono::steady_clock::time_point password_change_requested_at_;
|
||||
std::string pending_password_change_request_id_;
|
||||
std::string pending_local_password_;
|
||||
std::string password_change_error_;
|
||||
bool credential_recovery_in_progress_ = false;
|
||||
bool credential_recovery_attempt_pending_ = false;
|
||||
bool credential_recovery_retry_active_ = false;
|
||||
bool credential_recovery_promote_pending_ = false;
|
||||
bool credential_recovery_clear_pending_ = false;
|
||||
};
|
||||
|
||||
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_;
|
||||
// Cursor position is sampled asynchronously after remote mouse input has
|
||||
// been injected. Remember the input source so the sampled position is not
|
||||
// immediately echoed back to that same controller as stale feedback.
|
||||
std::mutex remote_pointer_input_mutex_;
|
||||
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
|
||||
last_remote_pointer_input_time_;
|
||||
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,
|
||||
PasswordChangeState,
|
||||
ConnectionState {};
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,458 @@
|
||||
// The legacy client uses ImGui::StyleColorsLight(). Keep the structural line
|
||||
// colors in one place so the Slint renderer produces the same visual weight.
|
||||
export global ImGuiLineStyle {
|
||||
out property <color> border: #0000004d;
|
||||
out property <color> separator: #6363639e;
|
||||
out property <color> divider: #0000007a;
|
||||
out property <color> control-separator: #b2b2b2;
|
||||
}
|
||||
|
||||
// ImGui loads a 32px font atlas and applies per-window font scales. These
|
||||
// logical sizes reproduce those visual tiers on Slint's HiDPI renderer.
|
||||
export global ImGuiFontStyle {
|
||||
out property <length> base: 12px;
|
||||
out property <length> section-title: 18px;
|
||||
out property <length> panel-label: 17px;
|
||||
out property <length> field-value: 22px;
|
||||
out property <length> prominent: 16px;
|
||||
out property <length> body: 11px;
|
||||
out property <length> small: 10px;
|
||||
}
|
||||
|
||||
// Font Awesome 6 Free Solid glyphs used by the legacy ImGui client. The font
|
||||
// data is registered for every Slint window by GuiApplication before show().
|
||||
export global FontAwesomeIcons {
|
||||
out property <string> angle-down: "\u{f107}";
|
||||
out property <string> angle-left: "\u{f104}";
|
||||
out property <string> angle-right: "\u{f105}";
|
||||
out property <string> angle-up: "\u{f106}";
|
||||
out property <string> arrow-right-long: "\u{f178}";
|
||||
out property <string> bars: "\u{f0c9}";
|
||||
out property <string> check: "\u{f00c}";
|
||||
out property <string> circle-arrow-up: "\u{f0aa}";
|
||||
out property <string> compress: "\u{f066}";
|
||||
out property <string> computer-mouse: "\u{f8cc}";
|
||||
out property <string> copy: "\u{f0c5}";
|
||||
out property <string> display: "\u{e163}";
|
||||
out property <string> expand: "\u{f065}";
|
||||
out property <string> eye: "\u{f06e}";
|
||||
out property <string> eye-slash: "\u{f070}";
|
||||
out property <string> folder-open: "\u{f07c}";
|
||||
out property <string> keyboard: "\u{f11c}";
|
||||
out property <string> minus: "\u{f068}";
|
||||
out property <string> pen: "\u{f304}";
|
||||
out property <string> signal: "\u{f012}";
|
||||
out property <string> shield-halved: "\u{f3ed}";
|
||||
out property <string> square-full: "\u{f45c}";
|
||||
out property <string> trash-can: "\u{f2ed}";
|
||||
out property <string> volume-high: "\u{f028}";
|
||||
out property <string> volume-xmark: "\u{f6a9}";
|
||||
out property <string> xmark: "\u{f00d}";
|
||||
}
|
||||
|
||||
export component IconButton inherits Rectangle {
|
||||
in property <string> icon;
|
||||
in property <string> tooltip;
|
||||
in property <bool> danger: false;
|
||||
in property <length> icon-size: 16px;
|
||||
in property <color> icon-color: #24272d;
|
||||
in property <color> normal-background: transparent;
|
||||
in property <color> hover-background: #edf0f5;
|
||||
in property <color> pressed-background: #d9dde5;
|
||||
in property <bool> slashed: false;
|
||||
callback clicked;
|
||||
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: touch.pressed ? (danger ? #d92d20 : root.pressed-background)
|
||||
: touch.has-hover ? (danger ? #f04438 : root.hover-background)
|
||||
: root.normal-background;
|
||||
border-radius: 4px;
|
||||
|
||||
Text {
|
||||
text: root.icon;
|
||||
color: touch.has-hover && root.danger ? white : root.icon-color;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: root.icon-size;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
if root.slashed: Text {
|
||||
text: "/";
|
||||
color: touch.has-hover && root.danger ? white : root.icon-color;
|
||||
font-size: root.icon-size + 7px;
|
||||
font-weight: 700;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
|
||||
if touch.has-hover && root.tooltip != "": Rectangle {
|
||||
x: min(root.width - self.width, 0px);
|
||||
y: root.height + 3px;
|
||||
width: tip.preferred-width + 12px;
|
||||
height: 24px;
|
||||
background: #22252be8;
|
||||
border-radius: 4px;
|
||||
z: 20;
|
||||
tip := Text {
|
||||
text: root.tooltip;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retained for non-font artwork. UI action icons use IconButton so the Slint
|
||||
// and ImGui clients share Font Awesome's glyph geometry.
|
||||
export component ImageButton inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <string> tooltip;
|
||||
in property <bool> danger: false;
|
||||
in property <length> icon-width: 16px;
|
||||
in property <length> icon-height: 16px;
|
||||
callback clicked;
|
||||
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: touch.pressed ? (danger ? #d92d20 : #d9dde5)
|
||||
: touch.has-hover ? (danger ? #f04438 : #edf0f5)
|
||||
: transparent;
|
||||
border-radius: 4px;
|
||||
|
||||
Image {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: root.icon-width;
|
||||
height: root.icon-height;
|
||||
source: root.icon;
|
||||
image-fit: contain;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
|
||||
if touch.has-hover && root.tooltip != "": Rectangle {
|
||||
x: min(root.width - self.width, 0px);
|
||||
y: root.height + 3px;
|
||||
width: tip.preferred-width + 12px;
|
||||
height: 24px;
|
||||
background: #22252be8;
|
||||
border-radius: 4px;
|
||||
z: 20;
|
||||
tip := Text {
|
||||
text: root.tooltip;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component CompactComboBox inherits Rectangle {
|
||||
in property <[string]> model;
|
||||
in-out property <int> current-index: 0;
|
||||
in property <bool> enabled: true;
|
||||
property <bool> popup-animation-enabled: false;
|
||||
property <float> popup-progress: 0.0;
|
||||
callback selected(int);
|
||||
|
||||
animate popup-progress {
|
||||
duration: 160ms;
|
||||
easing: ease-out;
|
||||
enabled: root.popup-animation-enabled;
|
||||
}
|
||||
|
||||
width: 73px;
|
||||
height: 24px;
|
||||
background: root.enabled ? white : #f1f1f1;
|
||||
border-width: 1px;
|
||||
border-color: #bfc2c7;
|
||||
border-radius: 3px;
|
||||
|
||||
Text {
|
||||
x: 5px;
|
||||
width: parent.width - 24px;
|
||||
text: root.model[root.current-index];
|
||||
color: root.enabled ? #202124 : #8b8f95;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - 21px;
|
||||
width: 20px;
|
||||
height: parent.height - 2px;
|
||||
y: 1px;
|
||||
background: root.enabled ? #9bc9f8 : #d7d9dc;
|
||||
Text {
|
||||
text: FontAwesomeIcons.angle-down;
|
||||
color: #202124;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
transform-rotation: combo-popup.is-open ? 180deg : 0deg;
|
||||
animate transform-rotation {
|
||||
duration: 140ms;
|
||||
easing: ease-out;
|
||||
enabled: combo-popup.is-open;
|
||||
}
|
||||
}
|
||||
}
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0.0;
|
||||
combo-popup.show();
|
||||
popup-open-timer.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Start on the next frame so the native popup first renders at zero
|
||||
// height, making the downward expansion visible on every backend.
|
||||
popup-open-timer := Timer {
|
||||
interval: 16ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
self.running = false;
|
||||
if combo-popup.is-open {
|
||||
root.popup-animation-enabled = true;
|
||||
root.popup-progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combo-popup := PopupWindow {
|
||||
x: 0px;
|
||||
y: root.height;
|
||||
width: root.width;
|
||||
height: root.model.length * 23px;
|
||||
close-policy: close-on-click-outside;
|
||||
|
||||
Rectangle {
|
||||
// Keep the reveal anchored to the popup's top edge while the
|
||||
// content settles downward into its final position.
|
||||
y: (1.0 - root.popup-progress) * -8px;
|
||||
width: parent.width;
|
||||
height: parent.height * root.popup-progress;
|
||||
opacity: root.popup-progress;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: #bfc2c7;
|
||||
border-radius: 3px;
|
||||
clip: true;
|
||||
for choice[index] in root.model: Rectangle {
|
||||
y: index * 23px;
|
||||
width: parent.width;
|
||||
height: 23px;
|
||||
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white;
|
||||
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
option-touch := TouchArea {
|
||||
clicked => {
|
||||
popup-open-timer.running = false;
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0.0;
|
||||
root.current-index = index;
|
||||
root.selected(index);
|
||||
combo-popup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
background: transparent;
|
||||
border-width: 1px;
|
||||
border-color: #bfc2c7;
|
||||
border-radius: 3px;
|
||||
z: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component ImGuiCheckBox inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
background: root.enabled ? white : #f1f1f1;
|
||||
border-width: 1px;
|
||||
border-color: check-touch.has-hover && root.enabled ? #8cbef2 : #b9bdc2;
|
||||
border-radius: 4px;
|
||||
if root.checked: Text {
|
||||
text: FontAwesomeIcons.check;
|
||||
color: root.enabled ? #2e86de : #8b8f95;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
check-touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.checked = !root.checked; }
|
||||
}
|
||||
}
|
||||
|
||||
export component MiniToggleSwitch inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
callback toggled(bool);
|
||||
|
||||
width: 29px;
|
||||
height: 16px;
|
||||
background: !root.enabled ? (root.checked ? #6e9ddacc : #9a9a9acc)
|
||||
: root.checked ? #2463c7 : #9a9a9a;
|
||||
border-radius: 8px;
|
||||
Rectangle {
|
||||
x: root.checked ? parent.width - self.width - 2px : 2px;
|
||||
y: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
}
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.checked = !root.checked; root.toggled(root.checked); }
|
||||
}
|
||||
}
|
||||
|
||||
export component CompactButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> primary: false;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
width: 34px;
|
||||
height: 24px;
|
||||
background: !root.enabled ? #eceef0
|
||||
: button-touch.pressed ? (root.primary ? #82b8f2 : #d8dadd)
|
||||
: button-touch.has-hover ? (root.primary ? #b8d9fb : #f0f1f2)
|
||||
: root.primary ? #a8d0fa : #e2e4e7;
|
||||
border-width: 1px;
|
||||
border-color: root.primary ? #9bc3ed : #c8cbd0;
|
||||
border-radius: 3px;
|
||||
Text {
|
||||
text: root.text;
|
||||
color: root.enabled ? #24272d : #8e9298;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
overflow: elide;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
button-touch := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } }
|
||||
}
|
||||
|
||||
export component PrimaryButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
min-width: 72px;
|
||||
height: 30px;
|
||||
background: !root.enabled ? #b8c1d1
|
||||
: touch.pressed ? #1849a9
|
||||
: touch.has-hover ? #2970d6
|
||||
: #2463c7;
|
||||
border-radius: 5px;
|
||||
|
||||
Text {
|
||||
text: root.text;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
export component SecondaryButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
min-width: 72px;
|
||||
height: 30px;
|
||||
background: !root.enabled ? #f1f2f4
|
||||
: touch.pressed ? #dfe3e9
|
||||
: touch.has-hover ? #f3f4f6 : white;
|
||||
border-width: 1px;
|
||||
border-color: #c9ced7;
|
||||
border-radius: 5px;
|
||||
clip: true;
|
||||
|
||||
Text {
|
||||
text: root.text;
|
||||
color: root.enabled ? #24272d : #949aa5;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
export component ToggleSwitch inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
callback toggled(bool);
|
||||
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: !root.enabled ? #d0d5dd : root.checked ? #2463c7 : #98a2b3;
|
||||
border-radius: self.height / 2;
|
||||
|
||||
Rectangle {
|
||||
x: root.checked ? parent.width - self.width - 3px : 3px;
|
||||
y: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 7px;
|
||||
background: white;
|
||||
animate x { duration: 100ms; easing: ease-in-out; }
|
||||
}
|
||||
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
root.checked = !root.checked;
|
||||
root.toggled(root.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component SectionTitle inherits Text {
|
||||
color: #00000080;
|
||||
font-size: ImGuiFontStyle.section-title;
|
||||
font-weight: 500;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
export component DialogSurface inherits Rectangle {
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 8px;
|
||||
}
|
||||