mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-07-23 07:40:11 +08:00
2671 lines
95 KiB
C++
2671 lines
95 KiB
C++
#include "application/gui_application.h"
|
|
|
|
#include <SDL3/SDL.h>
|
|
#include <libyuv.h>
|
|
#include <slint.h>
|
|
#include <tinyfiledialogs.h>
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <charconv>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <shared_mutex>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "crossdesk_ui.h"
|
|
#include "fa_solid_900.h"
|
|
#include "localization.h"
|
|
#if _WIN32
|
|
#include "platform/tray/win_tray.h"
|
|
#elif defined(__APPLE__)
|
|
#include "platform/tray/mac_tray.h"
|
|
#include "platform/window_drag.h"
|
|
#elif defined(__linux__)
|
|
#include "platform/tray/linux_tray.h"
|
|
#endif
|
|
#include "rd_log.h"
|
|
#include "version_checker.h"
|
|
|
|
namespace crossdesk {
|
|
namespace {
|
|
|
|
using namespace std::chrono_literals;
|
|
|
|
#if _WIN32
|
|
HICON LoadSlintTrayIcon() {
|
|
HMODULE module = GetModuleHandleW(nullptr);
|
|
HICON icon = reinterpret_cast<HICON>(
|
|
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
|
|
return icon ? icon : LoadIconW(nullptr, IDI_APPLICATION);
|
|
}
|
|
#endif
|
|
|
|
slint::SharedString UiText(std::string_view value) {
|
|
return slint::SharedString(value);
|
|
}
|
|
|
|
void RegisterFontAwesome(slint::Window &window) {
|
|
if (const auto error = window.window_handle().register_font_from_data(
|
|
fa_solid_900_ttf, fa_solid_900_ttf_len)) {
|
|
LOG_ERROR("Failed to register Font Awesome for Slint window: {}",
|
|
error->data());
|
|
}
|
|
}
|
|
|
|
std::string FormatPeerId(const char *id) {
|
|
if (!id) {
|
|
return {};
|
|
}
|
|
std::string compact(id);
|
|
if (compact.size() != 9) {
|
|
return compact;
|
|
}
|
|
compact.insert(3, " ");
|
|
compact.insert(7, " ");
|
|
return compact;
|
|
}
|
|
|
|
std::string FormatRemotePeerIdInput(std::string_view input) {
|
|
std::string digits;
|
|
digits.reserve(9);
|
|
for (const unsigned char ch : input) {
|
|
if (std::isdigit(ch) != 0 && digits.size() < 9) {
|
|
digits.push_back(static_cast<char>(ch));
|
|
}
|
|
}
|
|
if (digits.size() > 3) {
|
|
digits.insert(3, " ");
|
|
}
|
|
if (digits.size() > 7) {
|
|
digits.insert(7, " ");
|
|
}
|
|
return digits;
|
|
}
|
|
|
|
std::string CompactPeerId(std::string id) {
|
|
id.erase(std::remove_if(id.begin(), id.end(), [](unsigned char ch) {
|
|
return std::isspace(ch) != 0;
|
|
}),
|
|
id.end());
|
|
return id;
|
|
}
|
|
|
|
std::string Trim(std::string value) {
|
|
const auto first = std::find_if_not(value.begin(), value.end(),
|
|
[](unsigned char ch) {
|
|
return std::isspace(ch) != 0;
|
|
});
|
|
const auto last = std::find_if_not(value.rbegin(), value.rend(),
|
|
[](unsigned char ch) {
|
|
return std::isspace(ch) != 0;
|
|
})
|
|
.base();
|
|
if (first >= last) {
|
|
return {};
|
|
}
|
|
return std::string(first, last);
|
|
}
|
|
|
|
std::string CleanReleaseNotesForUi(std::string markdown) {
|
|
size_t line_start = 0;
|
|
while (line_start < markdown.size()) {
|
|
size_t content_start = line_start;
|
|
while (content_start < markdown.size() && markdown[content_start] == '#') {
|
|
++content_start;
|
|
}
|
|
if (content_start > line_start && content_start < markdown.size() &&
|
|
markdown[content_start] == ' ') {
|
|
++content_start;
|
|
}
|
|
if (content_start > line_start) {
|
|
markdown.erase(line_start, content_start - line_start);
|
|
}
|
|
const size_t newline = markdown.find('\n', line_start);
|
|
if (newline == std::string::npos) {
|
|
break;
|
|
}
|
|
line_start = newline + 1;
|
|
}
|
|
size_t marker = 0;
|
|
while ((marker = markdown.find("**", marker)) != std::string::npos) {
|
|
markdown.erase(marker, 2);
|
|
}
|
|
return markdown;
|
|
}
|
|
|
|
std::string DecodeDroppedPath(std::string text) {
|
|
const size_t line_end = text.find_first_of("\r\n");
|
|
if (line_end != std::string::npos) {
|
|
text.resize(line_end);
|
|
}
|
|
text = Trim(std::move(text));
|
|
if (text.rfind("file://localhost/", 0) == 0) {
|
|
text.erase(0, 16);
|
|
text.insert(text.begin(), '/');
|
|
} else if (text.rfind("file://", 0) == 0) {
|
|
text.erase(0, 7);
|
|
}
|
|
|
|
std::string decoded;
|
|
decoded.reserve(text.size());
|
|
auto hex_value = [](char value) -> int {
|
|
if (value >= '0' && value <= '9') return value - '0';
|
|
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
|
|
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
|
|
return -1;
|
|
};
|
|
for (size_t i = 0; i < text.size(); ++i) {
|
|
if (text[i] == '%' && i + 2 < text.size()) {
|
|
const int high = hex_value(text[i + 1]);
|
|
const int low = hex_value(text[i + 2]);
|
|
if (high >= 0 && low >= 0) {
|
|
decoded.push_back(static_cast<char>((high << 4) | low));
|
|
i += 2;
|
|
continue;
|
|
}
|
|
}
|
|
decoded.push_back(text[i]);
|
|
}
|
|
#if defined(_WIN32)
|
|
if (decoded.size() >= 3 && decoded[0] == '/' &&
|
|
std::isalpha(static_cast<unsigned char>(decoded[1])) &&
|
|
decoded[2] == ':') {
|
|
decoded.erase(decoded.begin());
|
|
}
|
|
#endif
|
|
return decoded;
|
|
}
|
|
|
|
std::optional<int> ParsePort(const slint::SharedString &text) {
|
|
const std::string value(text);
|
|
int port = 0;
|
|
const auto [end, error] =
|
|
std::from_chars(value.data(), value.data() + value.size(), port);
|
|
if (error != std::errc{} || end != value.data() + value.size() || port <= 0 ||
|
|
port > 65535) {
|
|
return std::nullopt;
|
|
}
|
|
return port;
|
|
}
|
|
|
|
std::string ConnectionStatusText(ConnectionStatus status, int language) {
|
|
switch (status) {
|
|
case ConnectionStatus::Connected:
|
|
return localization::p2p_connected[language];
|
|
case ConnectionStatus::Connecting:
|
|
return localization::p2p_connecting[language];
|
|
case ConnectionStatus::Gathering:
|
|
return localization::p2p_gathering[language];
|
|
case ConnectionStatus::Disconnected:
|
|
return localization::p2p_disconnected[language];
|
|
case ConnectionStatus::NoSuchTransmissionId:
|
|
return localization::no_such_id[language];
|
|
case ConnectionStatus::IncorrectPassword:
|
|
return localization::reinput_password[language];
|
|
case ConnectionStatus::Closed:
|
|
return localization::p2p_closed[language];
|
|
case ConnectionStatus::RemoteUnavailable:
|
|
return localization::device_offline[language];
|
|
case ConnectionStatus::Failed:
|
|
default:
|
|
return localization::p2p_failed[language];
|
|
}
|
|
}
|
|
|
|
std::string FormatBitrate(uint32_t bits_per_second) {
|
|
char text[48];
|
|
if (bits_per_second < 1000) {
|
|
std::snprintf(text, sizeof(text), "%u bps", bits_per_second);
|
|
} else if (bits_per_second < 1000000) {
|
|
std::snprintf(text, sizeof(text), "%u kbps", bits_per_second / 1000);
|
|
} else {
|
|
std::snprintf(text, sizeof(text), "%.1f mbps",
|
|
bits_per_second / 1000000.0);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
std::string FormatTransferRate(uint32_t bits_per_second) {
|
|
return bits_per_second == 0 ? std::string{} : FormatBitrate(bits_per_second);
|
|
}
|
|
|
|
std::string FormatLossRate(float loss_rate) {
|
|
if (loss_rate < 0.01f) {
|
|
return "0%";
|
|
}
|
|
char text[24];
|
|
std::snprintf(text, sizeof(text), "%.0f%%", loss_rate * 100.0f);
|
|
return text;
|
|
}
|
|
|
|
std::string FormatFileSize(uint64_t bytes) {
|
|
char text[48];
|
|
if (bytes < 1024) {
|
|
std::snprintf(text, sizeof(text), "%llu B",
|
|
static_cast<unsigned long long>(bytes));
|
|
} else if (bytes < 1024 * 1024) {
|
|
std::snprintf(text, sizeof(text), "%.2f KB", bytes / 1024.0);
|
|
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
|
|
std::snprintf(text, sizeof(text), "%.2f MB",
|
|
bytes / (1024.0 * 1024.0));
|
|
} else {
|
|
std::snprintf(text, sizeof(text), "%.2f GB",
|
|
bytes / (1024.0 * 1024.0 * 1024.0));
|
|
}
|
|
return text;
|
|
}
|
|
|
|
uint32_t DecodeFirstUtf8CodePoint(const std::string &text) {
|
|
if (text.empty()) {
|
|
return 0;
|
|
}
|
|
const auto *bytes = reinterpret_cast<const unsigned char *>(text.data());
|
|
if (bytes[0] < 0x80) {
|
|
return bytes[0];
|
|
}
|
|
if ((bytes[0] & 0xE0) == 0xC0 && text.size() >= 2) {
|
|
return ((bytes[0] & 0x1F) << 6) | (bytes[1] & 0x3F);
|
|
}
|
|
if ((bytes[0] & 0xF0) == 0xE0 && text.size() >= 3) {
|
|
return ((bytes[0] & 0x0F) << 12) | ((bytes[1] & 0x3F) << 6) |
|
|
(bytes[2] & 0x3F);
|
|
}
|
|
if ((bytes[0] & 0xF8) == 0xF0 && text.size() >= 4) {
|
|
return ((bytes[0] & 0x07) << 18) | ((bytes[1] & 0x3F) << 12) |
|
|
((bytes[2] & 0x3F) << 6) | (bytes[3] & 0x3F);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int SlintKeyToWindowsVk(const std::string &text) {
|
|
const uint32_t key = DecodeFirstUtf8CodePoint(text);
|
|
if (key >= 'a' && key <= 'z') {
|
|
return static_cast<int>(key - 'a' + 'A');
|
|
}
|
|
if ((key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
|
|
return static_cast<int>(key);
|
|
}
|
|
if (key >= 0xF704 && key <= 0xF71B) {
|
|
return 0x70 + static_cast<int>(key - 0xF704);
|
|
}
|
|
|
|
switch (key) {
|
|
case 0x08:
|
|
return 0x08;
|
|
case 0x09:
|
|
case 0x19:
|
|
return 0x09;
|
|
case 0x0A:
|
|
return 0x0D;
|
|
case 0x1B:
|
|
return 0x1B;
|
|
case 0x7F:
|
|
return 0x2E;
|
|
case 0x10:
|
|
return 0xA0;
|
|
case 0x15:
|
|
return 0xA1;
|
|
case 0x11:
|
|
return 0xA2;
|
|
case 0x16:
|
|
return 0xA3;
|
|
case 0x12:
|
|
return 0xA4;
|
|
case 0x13:
|
|
return 0xA5;
|
|
case 0x17:
|
|
return 0x5B;
|
|
case 0x18:
|
|
return 0x5C;
|
|
case 0x14:
|
|
return 0x14;
|
|
case 0x20:
|
|
return 0x20;
|
|
case 0xF700:
|
|
return 0x26;
|
|
case 0xF701:
|
|
return 0x28;
|
|
case 0xF702:
|
|
return 0x25;
|
|
case 0xF703:
|
|
return 0x27;
|
|
case 0xF727:
|
|
return 0x2D;
|
|
case 0xF729:
|
|
return 0x24;
|
|
case 0xF72B:
|
|
return 0x23;
|
|
case 0xF72C:
|
|
return 0x21;
|
|
case 0xF72D:
|
|
return 0x22;
|
|
case 0xF72F:
|
|
return 0x91;
|
|
case 0xF730:
|
|
return 0x13;
|
|
case 0xF731:
|
|
return 0x2C;
|
|
case 0xF734:
|
|
return 0xB2;
|
|
case 0xF735:
|
|
return 0x5D;
|
|
case 0xF748:
|
|
return 0xA6;
|
|
case ';':
|
|
case ':':
|
|
return 0xBA;
|
|
case '\'':
|
|
case '"':
|
|
return 0xDE;
|
|
case '`':
|
|
case '~':
|
|
return 0xC0;
|
|
case ',':
|
|
case '<':
|
|
return 0xBC;
|
|
case '.':
|
|
case '>':
|
|
return 0xBE;
|
|
case '/':
|
|
case '?':
|
|
return 0xBF;
|
|
case '\\':
|
|
case '|':
|
|
return 0xDC;
|
|
case '[':
|
|
case '{':
|
|
return 0xDB;
|
|
case ']':
|
|
case '}':
|
|
return 0xDD;
|
|
case '-':
|
|
case '_':
|
|
return 0xBD;
|
|
case '=':
|
|
case '+':
|
|
return 0xBB;
|
|
case '!':
|
|
return '1';
|
|
case '@':
|
|
return '2';
|
|
case '#':
|
|
return '3';
|
|
case '$':
|
|
return '4';
|
|
case '%':
|
|
return '5';
|
|
case '^':
|
|
return '6';
|
|
case '&':
|
|
return '7';
|
|
case '*':
|
|
return '8';
|
|
case '(':
|
|
return '9';
|
|
case ')':
|
|
return '0';
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
struct WindowDragState {
|
|
bool active = false;
|
|
bool use_global_pointer = false;
|
|
float pointer_start_x = 0;
|
|
float pointer_start_y = 0;
|
|
float last_local_x = 0;
|
|
float last_local_y = 0;
|
|
float global_to_physical_scale = 1;
|
|
slint::PhysicalPosition window_start;
|
|
slint::PhysicalPosition last_target;
|
|
};
|
|
|
|
bool CanUseGlobalPointerPosition() {
|
|
const char *driver = SDL_GetCurrentVideoDriver();
|
|
if (driver == nullptr) {
|
|
return false;
|
|
}
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
// Wayland deliberately does not expose global pointer coordinates or allow
|
|
// clients to position top-level windows.
|
|
return std::strcmp(driver, "wayland") != 0;
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
float ServerWindowLogicalWidth(int language) {
|
|
return language == 0 ? 250.0f : language == 1 ? 330.0f : 430.0f;
|
|
}
|
|
|
|
float ServerWindowLogicalHeight(bool collapsed) {
|
|
return collapsed ? 30.0f : 150.0f;
|
|
}
|
|
|
|
constexpr float kStreamWindowLogicalWidth = 1280.0f;
|
|
constexpr float kStreamWindowLogicalHeight = 720.0f;
|
|
|
|
SDL_DisplayID DisplayForSlintWindow(const slint::Window &window) {
|
|
const float scale = std::max(window.scale_factor(), 0.01f);
|
|
const auto position = window.position();
|
|
const auto size = window.size();
|
|
const SDL_Point center{
|
|
static_cast<int>(std::lround(position.x / scale +
|
|
size.width / (2.0f * scale))),
|
|
static_cast<int>(std::lround(position.y / scale +
|
|
size.height / (2.0f * scale)))};
|
|
const SDL_DisplayID display = SDL_GetDisplayForPoint(¢er);
|
|
return display != 0 ? display : SDL_GetPrimaryDisplay();
|
|
}
|
|
|
|
bool PositionWindowAtCenter(slint::Window &window,
|
|
const slint::Window &anchor,
|
|
float logical_width, float logical_height) {
|
|
if (!CanUseGlobalPointerPosition()) {
|
|
return false;
|
|
}
|
|
|
|
const SDL_DisplayID display = DisplayForSlintWindow(anchor);
|
|
SDL_Rect usable_bounds{};
|
|
if (display == 0 ||
|
|
!SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
|
|
LOG_WARN("Unable to obtain usable display bounds for stream window: {}",
|
|
SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
const float target_x = static_cast<float>(usable_bounds.x) +
|
|
(static_cast<float>(usable_bounds.w) - logical_width) /
|
|
2.0f;
|
|
const float target_y = static_cast<float>(usable_bounds.y) +
|
|
(static_cast<float>(usable_bounds.h) - logical_height) /
|
|
2.0f;
|
|
window.set_position(slint::LogicalPosition(
|
|
slint::Point<float>{target_x, target_y}));
|
|
return true;
|
|
}
|
|
|
|
bool PositionWindowAtBottomRight(slint::Window &window, float logical_width,
|
|
float logical_height) {
|
|
if (!CanUseGlobalPointerPosition()) {
|
|
return false;
|
|
}
|
|
|
|
SDL_DisplayID display = 0;
|
|
const float scale = std::max(window.scale_factor(), 0.01f);
|
|
const auto position = window.position();
|
|
const auto size = window.size();
|
|
const SDL_Point center{
|
|
static_cast<int>(std::lround(position.x / scale +
|
|
size.width / (2.0f * scale))),
|
|
static_cast<int>(std::lround(position.y / scale +
|
|
size.height / (2.0f * scale)))};
|
|
display = SDL_GetDisplayForPoint(¢er);
|
|
if (display == 0) {
|
|
display = SDL_GetPrimaryDisplay();
|
|
}
|
|
|
|
SDL_Rect usable_bounds{};
|
|
if (display == 0 ||
|
|
!SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
|
|
LOG_WARN("Unable to obtain usable display bounds for server window: {}",
|
|
SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
const float target_x =
|
|
static_cast<float>(usable_bounds.x) +
|
|
std::max(0.0f, static_cast<float>(usable_bounds.w) - logical_width);
|
|
const float target_y =
|
|
static_cast<float>(usable_bounds.y) +
|
|
std::max(0.0f, static_cast<float>(usable_bounds.h) - logical_height);
|
|
window.set_position(slint::LogicalPosition(
|
|
slint::Point<float>{target_x, target_y}));
|
|
return true;
|
|
}
|
|
|
|
template <typename WindowHandle>
|
|
void DragWindow(WindowHandle &component, int phase, float mouse_x,
|
|
float mouse_y, WindowDragState &state) {
|
|
if (phase == 1) {
|
|
#if defined(__APPLE__)
|
|
// Let AppKit run the native move loop. This preserves macOS window
|
|
// snapping, Spaces, multi-display, and high-DPI behavior without manually
|
|
// updating the window position for every pointer event.
|
|
if (!StartNativeWindowDrag()) {
|
|
LOG_WARN("Unable to start native macOS window drag");
|
|
}
|
|
state.active = false;
|
|
return;
|
|
#endif
|
|
auto &window = component->window();
|
|
state.active = true;
|
|
state.last_local_x = mouse_x;
|
|
state.last_local_y = mouse_y;
|
|
state.window_start = window.position();
|
|
state.last_target = state.window_start;
|
|
state.use_global_pointer = CanUseGlobalPointerPosition();
|
|
if (state.use_global_pointer) {
|
|
SDL_GetGlobalMouseState(&state.pointer_start_x, &state.pointer_start_y);
|
|
#if defined(__APPLE__)
|
|
// Cocoa reports global pointer positions in logical screen points,
|
|
// while Slint/winit accepts physical window positions.
|
|
state.global_to_physical_scale = window.scale_factor();
|
|
#else
|
|
state.global_to_physical_scale = 1.0f;
|
|
#endif
|
|
}
|
|
return;
|
|
}
|
|
if (phase == 0) {
|
|
state.active = false;
|
|
return;
|
|
}
|
|
if (!state.active || phase != 2) {
|
|
return;
|
|
}
|
|
|
|
auto &window = component->window();
|
|
if (state.use_global_pointer) {
|
|
float pointer_x = 0;
|
|
float pointer_y = 0;
|
|
SDL_GetGlobalMouseState(&pointer_x, &pointer_y);
|
|
const int delta_x = static_cast<int>(std::lround(
|
|
(pointer_x - state.pointer_start_x) *
|
|
state.global_to_physical_scale));
|
|
const int delta_y = static_cast<int>(std::lround(
|
|
(pointer_y - state.pointer_start_y) *
|
|
state.global_to_physical_scale));
|
|
const slint::PhysicalPosition target(slint::Point<int32_t>{
|
|
state.window_start.x + delta_x, state.window_start.y + delta_y});
|
|
if (target.x != state.last_target.x || target.y != state.last_target.y) {
|
|
window.set_position(target);
|
|
state.last_target = target;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Retain the previous behavior for backends without global pointer access.
|
|
const float scale = window.scale_factor();
|
|
const auto position = window.position();
|
|
const int delta_x = static_cast<int>(
|
|
std::lround((mouse_x - state.last_local_x) * scale));
|
|
const int delta_y = static_cast<int>(
|
|
std::lround((mouse_y - state.last_local_y) * scale));
|
|
if (delta_x != 0 || delta_y != 0) {
|
|
window.set_position(
|
|
slint::PhysicalPosition(slint::Point<int32_t>{
|
|
position.x + delta_x, position.y + delta_y}));
|
|
}
|
|
state.last_local_x = mouse_x;
|
|
state.last_local_y = mouse_y;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
struct GuiApplication::SlintUi {
|
|
slint::ComponentHandle<ui::MainWindow> main = ui::MainWindow::create();
|
|
std::optional<slint::ComponentHandle<ui::StreamWindow>> stream;
|
|
std::optional<slint::ComponentHandle<ui::ServerWindow>> server;
|
|
std::shared_ptr<slint::VectorModel<ui::RecentConnection>> recent_model =
|
|
std::make_shared<slint::VectorModel<ui::RecentConnection>>();
|
|
std::shared_ptr<slint::VectorModel<ui::StreamTab>> tab_model =
|
|
std::make_shared<slint::VectorModel<ui::StreamTab>>();
|
|
std::shared_ptr<slint::VectorModel<slint::SharedString>> display_model =
|
|
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
|
std::shared_ptr<slint::VectorModel<ui::FileTransferEntry>> transfer_model =
|
|
std::make_shared<slint::VectorModel<ui::FileTransferEntry>>();
|
|
std::shared_ptr<slint::VectorModel<ui::NetworkStatsRow>> stats_model =
|
|
std::make_shared<slint::VectorModel<ui::NetworkStatsRow>>();
|
|
std::shared_ptr<slint::VectorModel<ui::ControllerEntry>> controller_model =
|
|
std::make_shared<slint::VectorModel<ui::ControllerEntry>>();
|
|
std::shared_ptr<slint::VectorModel<slint::SharedString>> controller_name_model =
|
|
std::make_shared<slint::VectorModel<slint::SharedString>>();
|
|
std::unordered_map<std::string, uint64_t> displayed_frame_sequence;
|
|
std::vector<std::string> tab_order;
|
|
std::vector<std::string> tab_ids;
|
|
std::string tab_model_signature;
|
|
std::vector<std::string> controller_ids;
|
|
std::string connection_dialog_remote_id;
|
|
bool connection_password_initialized = false;
|
|
bool portable_service_dialog_initialized = false;
|
|
std::string recent_model_signature;
|
|
slint::Timer timer;
|
|
WindowDragState server_drag;
|
|
int main_native_titlebar_attempts = 30;
|
|
int stream_live_resize_configuration_attempts = 0;
|
|
int stream_initial_position_attempts = 0;
|
|
int server_initial_position_attempts = 0;
|
|
int localized_language = -1;
|
|
bool capture_mode = false;
|
|
std::string capture_page;
|
|
std::chrono::steady_clock::time_point last_clipboard_poll{};
|
|
#if _WIN32
|
|
std::unique_ptr<WinTray> tray;
|
|
#elif defined(__APPLE__)
|
|
std::unique_ptr<MacTray> tray;
|
|
#elif defined(__linux__)
|
|
std::unique_ptr<LinuxTray> tray;
|
|
#endif
|
|
};
|
|
|
|
GuiApplication::GuiApplication() = default;
|
|
GuiApplication::~GuiApplication() = default;
|
|
|
|
int GuiApplication::Run() {
|
|
path_manager_ = std::make_unique<PathManager>("CrossDesk");
|
|
if (!path_manager_) {
|
|
return -1;
|
|
}
|
|
exec_log_path_ = path_manager_->GetLogPath().string();
|
|
dll_log_path_ = exec_log_path_;
|
|
cache_path_ = path_manager_->GetCachePath().string();
|
|
config_center_ =
|
|
std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
|
|
|
InitializeLogger();
|
|
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
|
|
|
|
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
|
sizeof(signal_server_ip_self_) - 1);
|
|
const int signal_port = config_center_->GetSignalServerPort();
|
|
if (signal_port > 0) {
|
|
std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_),
|
|
"%d", signal_port);
|
|
}
|
|
|
|
latest_version_info_ = CheckUpdate();
|
|
if (!latest_version_info_.empty()) {
|
|
std::string version;
|
|
if (latest_version_info_.contains("latest_version") &&
|
|
latest_version_info_["latest_version"].is_string()) {
|
|
version = latest_version_info_["latest_version"].get<std::string>();
|
|
} else if (latest_version_info_.contains("version") &&
|
|
latest_version_info_["version"].is_string()) {
|
|
version = latest_version_info_["version"].get<std::string>();
|
|
}
|
|
latest_version_ = version.empty() ? std::string{} : "v" + version;
|
|
if (latest_version_info_.contains("releaseNotes") &&
|
|
latest_version_info_["releaseNotes"].is_string()) {
|
|
release_notes_ =
|
|
latest_version_info_["releaseNotes"].get<std::string>();
|
|
}
|
|
if (latest_version_info_.contains("releaseName") &&
|
|
latest_version_info_["releaseName"].is_string()) {
|
|
release_name_ = latest_version_info_["releaseName"].get<std::string>();
|
|
}
|
|
if (latest_version_info_.contains("releaseDate") &&
|
|
latest_version_info_["releaseDate"].is_string()) {
|
|
release_date_ = latest_version_info_["releaseDate"].get<std::string>();
|
|
}
|
|
update_available_ =
|
|
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
|
}
|
|
|
|
InitializeSettings();
|
|
if (!InitializeSDL()) {
|
|
return -1;
|
|
}
|
|
InitializeModules();
|
|
InitializeUi();
|
|
|
|
ui_->timer.start(slint::TimerMode::Repeated, 16ms, [this] { Tick(); });
|
|
if (ui_->capture_mode &&
|
|
(ui_->capture_page == "stream" || ui_->capture_page == "server")) {
|
|
slint::run_event_loop();
|
|
} else if (ui_->capture_mode) {
|
|
ui_->main->run();
|
|
} else {
|
|
// The native tray icon is managed outside Slint, so Slint cannot count it
|
|
// when deciding whether the last hidden window should stop the event loop.
|
|
// Keep the loop alive until the tray's explicit Exit action requests quit.
|
|
ui_->main->show();
|
|
slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
|
|
}
|
|
Cleanup();
|
|
return 0;
|
|
}
|
|
|
|
void GuiApplication::InitializeLogger() { InitLogger(exec_log_path_); }
|
|
|
|
void GuiApplication::InitializeSettings() {
|
|
settings_.Load();
|
|
settings_.LoadRecentConnectionAliases();
|
|
localization_language_index_ =
|
|
localization::detail::ClampLanguageIndex(language_button_value_);
|
|
language_button_value_ = localization_language_index_;
|
|
localization_language_ =
|
|
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
|
|
}
|
|
|
|
bool GuiApplication::InitializeSDL() {
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
if (!getenv("SDL_AUDIODRIVER")) {
|
|
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
|
|
}
|
|
#endif
|
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
|
|
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
if (const SDL_DisplayMode *mode = SDL_GetCurrentDisplayMode(0)) {
|
|
screen_width_ = mode->w;
|
|
screen_height_ = mode->h;
|
|
}
|
|
clipboard_.Initialize();
|
|
return true;
|
|
}
|
|
|
|
void GuiApplication::InitializeModules() {
|
|
if (modules_inited_) {
|
|
return;
|
|
}
|
|
devices_.Initialize();
|
|
CreateConnectionPeer();
|
|
modules_inited_ = true;
|
|
}
|
|
|
|
void GuiApplication::InitializeUi() {
|
|
ui_ = std::make_unique<SlintUi>();
|
|
RegisterFontAwesome(ui_->main->window());
|
|
#ifdef CROSSDESK_DEBUG
|
|
auto read_capture_override = [](const char *path) -> std::string {
|
|
std::ifstream input(path);
|
|
std::string value;
|
|
std::getline(input, value);
|
|
return value;
|
|
};
|
|
if (const char *capture = std::getenv("CROSSDESK_UI_CAPTURE")) {
|
|
ui_->capture_mode = std::string_view(capture) != "0";
|
|
}
|
|
if (const char *page = std::getenv("CROSSDESK_UI_CAPTURE_PAGE")) {
|
|
ui_->capture_page = page;
|
|
}
|
|
std::string capture_language_override;
|
|
if (ui_->capture_mode) {
|
|
if (const auto page = read_capture_override(
|
|
"/tmp/crossdesk-ui-capture-page");
|
|
!page.empty()) {
|
|
ui_->capture_page = page;
|
|
}
|
|
capture_language_override = read_capture_override(
|
|
"/tmp/crossdesk-ui-capture-language");
|
|
}
|
|
const char *language = capture_language_override.empty()
|
|
? std::getenv("CROSSDESK_UI_CAPTURE_LANGUAGE")
|
|
: capture_language_override.c_str();
|
|
if (language) {
|
|
int capture_language = 0;
|
|
const auto [end, error] = std::from_chars(
|
|
language, language + std::strlen(language), capture_language);
|
|
if (error == std::errc{} && end == language + std::strlen(language)) {
|
|
localization_language_index_ =
|
|
localization::detail::ClampLanguageIndex(capture_language);
|
|
language_button_value_ = localization_language_index_;
|
|
localization_language_ = static_cast<ConfigCenter::LANGUAGE>(
|
|
localization_language_index_);
|
|
}
|
|
}
|
|
#endif
|
|
ui_->main->set_recent_connections(ui_->recent_model);
|
|
ResetSettingsUi();
|
|
BindMainCallbacks();
|
|
InitializeSystemTray();
|
|
UpdateLocalization();
|
|
if (ui_->capture_mode) {
|
|
const bool detected_update = update_available_;
|
|
update_available_ = false;
|
|
if (ui_->capture_page == "settings") {
|
|
ui_->main->set_settings_open(true);
|
|
} else if (ui_->capture_page == "self-hosted") {
|
|
ui_->main->set_settings_open(true);
|
|
ui_->main->set_self_host_settings_open(true);
|
|
} else if (ui_->capture_page == "about") {
|
|
update_available_ = detected_update;
|
|
ui_->main->set_update_open(false);
|
|
ui_->main->set_about_open(true);
|
|
} else if (ui_->capture_page == "update") {
|
|
update_available_ = detected_update;
|
|
ui_->main->set_update_open(true);
|
|
} else if (ui_->capture_page == "reset-password") {
|
|
ui_->main->set_reset_password_open(true);
|
|
} else if (ui_->capture_page == "reset-password-invalid") {
|
|
ui_->main->set_reset_password_open(true);
|
|
ui_->main->set_reset_password_invalid(true);
|
|
ui_->main->set_new_password_input("123");
|
|
} else if (ui_->capture_page == "alias") {
|
|
ui_->main->set_alias_input("Office workstation");
|
|
ui_->main->set_alias_open(true);
|
|
} else if (ui_->capture_page == "delete") {
|
|
ui_->main->set_delete_open(true);
|
|
} else if (ui_->capture_page == "offline") {
|
|
ui_->main->set_offline_warning(
|
|
UiText(localization::device_offline[localization_language_index_]));
|
|
} else if (ui_->capture_page == "connection") {
|
|
ui_->main->set_connection_status_text(
|
|
UiText(localization::p2p_connecting[localization_language_index_]));
|
|
ui_->main->set_connection_pending(true);
|
|
ui_->main->set_connection_dialog_open(true);
|
|
} else if (ui_->capture_page == "connection-password") {
|
|
ui_->main->set_connection_status_text(
|
|
UiText(localization::reinput_password[localization_language_index_]));
|
|
ui_->main->set_connection_password_required(true);
|
|
ui_->main->set_connection_dialog_open(true);
|
|
} else if (ui_->capture_page == "service") {
|
|
ui_->main->set_portable_service_dialog_open(true);
|
|
} else if (ui_->capture_page == "service-notice") {
|
|
ui_->main->set_portable_service_suppressed_notice_open(true);
|
|
}
|
|
}
|
|
SyncMainWindow();
|
|
|
|
if (ui_->capture_mode && ui_->capture_page == "stream") {
|
|
ui_->stream.emplace(ui::StreamWindow::create());
|
|
RegisterFontAwesome((*ui_->stream)->window());
|
|
(*ui_->stream)->set_tabs(ui_->tab_model);
|
|
(*ui_->stream)->set_displays(ui_->display_model);
|
|
(*ui_->stream)->set_file_transfers(ui_->transfer_model);
|
|
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
|
ui::StreamTab preview_tab;
|
|
preview_tab.remote_id = "589173341";
|
|
preview_tab.title = "Mac";
|
|
preview_tab.connected = true;
|
|
ui_->tab_model->set_vector({preview_tab});
|
|
ui_->display_model->set_vector({slint::SharedString("Display 1")});
|
|
ui_->localized_language = -1;
|
|
UpdateLocalization();
|
|
(*ui_->stream)->set_status_text(
|
|
UiText(localization::p2p_connected[localization_language_index_]));
|
|
#ifdef CROSSDESK_DEBUG
|
|
// A bordered 16:9 frame makes stream scaling/cropping regressions visible
|
|
// in the existing capture mode without requiring a live remote session.
|
|
constexpr int preview_width = 640;
|
|
constexpr int preview_height = 360;
|
|
slint::SharedPixelBuffer<slint::Rgb8Pixel> preview_pixels(preview_width,
|
|
preview_height);
|
|
auto *preview_data =
|
|
reinterpret_cast<uint8_t *>(preview_pixels.begin());
|
|
for (int y = 0; y < preview_height; ++y) {
|
|
for (int x = 0; x < preview_width; ++x) {
|
|
const bool border = x < 8 || y < 8 || x >= preview_width - 8 ||
|
|
y >= preview_height - 8;
|
|
const size_t offset =
|
|
(static_cast<size_t>(y) * preview_width + x) * 3;
|
|
preview_data[offset] = border ? 240 : static_cast<uint8_t>(
|
|
35 + 150 * x /
|
|
preview_width);
|
|
preview_data[offset + 1] = border ? 70 : static_cast<uint8_t>(
|
|
35 + 150 * y /
|
|
preview_height);
|
|
preview_data[offset + 2] = border ? 70 : 90;
|
|
}
|
|
}
|
|
(*ui_->stream)->set_frame(slint::Image(std::move(preview_pixels)));
|
|
(*ui_->stream)->set_has_frame(true);
|
|
(*ui_->stream)->set_status_text("");
|
|
#endif
|
|
BindStreamCallbacks();
|
|
(*ui_->stream)->show();
|
|
PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
|
|
kStreamWindowLogicalWidth,
|
|
kStreamWindowLogicalHeight);
|
|
ui_->main->hide();
|
|
} else if (ui_->capture_mode && ui_->capture_page == "server") {
|
|
ui_->server.emplace(ui::ServerWindow::create());
|
|
RegisterFontAwesome((*ui_->server)->window());
|
|
(*ui_->server)->set_controllers(ui_->controller_model);
|
|
(*ui_->server)->set_controller_names(ui_->controller_name_model);
|
|
(*ui_->server)->set_language_index(localization_language_index_);
|
|
const float server_width =
|
|
ServerWindowLogicalWidth(localization_language_index_);
|
|
const float server_height = ServerWindowLogicalHeight(false);
|
|
(*ui_->server)->window().set_size(slint::LogicalSize(
|
|
slint::Size<float>{server_width, server_height}));
|
|
ui::ControllerEntry preview_controller;
|
|
preview_controller.remote_id = "589173341";
|
|
preview_controller.display_name = "Mac";
|
|
ui_->controller_model->set_vector({preview_controller});
|
|
ui_->controller_name_model->set_vector({slint::SharedString("Mac")});
|
|
(*ui_->server)->set_controller_label(
|
|
UiText(localization::controller[localization_language_index_]));
|
|
(*ui_->server)->set_connection_label(
|
|
UiText(localization::connection_status[localization_language_index_]));
|
|
(*ui_->server)->set_connection_status(
|
|
UiText(localization::p2p_connected[localization_language_index_]));
|
|
(*ui_->server)->set_file_transfer_label(
|
|
UiText(localization::file_transfer[localization_language_index_]));
|
|
(*ui_->server)->set_select_file_label(
|
|
UiText(localization::select_file[localization_language_index_]));
|
|
BindServerCallbacks();
|
|
(*ui_->server)->show();
|
|
PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
|
|
server_height);
|
|
ui_->main->hide();
|
|
}
|
|
}
|
|
|
|
void GuiApplication::InitializeSystemTray() {
|
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
|
if (!ui_) {
|
|
return;
|
|
}
|
|
auto show_window = [this] {
|
|
if (!ui_) {
|
|
return;
|
|
}
|
|
ui_->main->show();
|
|
ui_->main->window().set_minimized(false);
|
|
#if defined(__APPLE__)
|
|
// A window restored from the tray may only obtain its NSWindow after the
|
|
// callback returns, so let the regular UI tick configure it.
|
|
ui_->main_native_titlebar_attempts = 30;
|
|
#endif
|
|
};
|
|
auto hide_window = [this] {
|
|
if (ui_) {
|
|
ui_->main->hide();
|
|
}
|
|
};
|
|
auto open_settings = [this] {
|
|
if (!ui_) {
|
|
return;
|
|
}
|
|
ui_->main->show();
|
|
ui_->main->window().set_minimized(false);
|
|
ui_->main->set_settings_open(true);
|
|
#if defined(__APPLE__)
|
|
ui_->main_native_titlebar_attempts = 30;
|
|
#endif
|
|
};
|
|
auto exit_app = [this] {
|
|
exit_ = true;
|
|
slint::quit_event_loop();
|
|
};
|
|
#if _WIN32
|
|
ui_->tray = std::make_unique<WinTray>(
|
|
std::move(show_window), std::move(hide_window), std::move(exit_app),
|
|
LoadSlintTrayIcon(), L"CrossDesk", localization_language_index_);
|
|
#elif defined(__APPLE__)
|
|
ui_->tray = std::make_unique<MacTray>(
|
|
std::move(show_window), std::move(hide_window),
|
|
std::move(open_settings), std::move(exit_app), "CrossDesk",
|
|
localization_language_index_);
|
|
#elif defined(__linux__)
|
|
ui_->tray = std::make_unique<LinuxTray>(
|
|
std::move(show_window), std::move(hide_window), std::move(exit_app),
|
|
"CrossDesk", localization_language_index_);
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
bool GuiApplication::MinimizeMainWindowToTray() {
|
|
if (!enable_minimize_to_tray_ || !ui_) {
|
|
return false;
|
|
}
|
|
#if _WIN32 || defined(__APPLE__)
|
|
if (!ui_->tray) {
|
|
return false;
|
|
}
|
|
ui_->tray->MinimizeToTray();
|
|
return true;
|
|
#elif defined(__linux__)
|
|
return ui_->tray && ui_->tray->MinimizeToTray();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void GuiApplication::ResetSettingsUi() {
|
|
if (!ui_) {
|
|
return;
|
|
}
|
|
ui_->main->set_language_index(language_button_value_);
|
|
ui_->main->set_video_quality_index(video_quality_button_value_);
|
|
ui_->main->set_frame_rate_index(video_frame_rate_button_value_);
|
|
ui_->main->set_codec_index(video_encode_format_button_value_);
|
|
ui_->main->set_hardware_codec_enabled(enable_hardware_video_codec_);
|
|
ui_->main->set_turn_enabled(enable_turn_);
|
|
ui_->main->set_srtp_enabled(enable_srtp_);
|
|
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
|
|
ui_->main->set_autostart_enabled(enable_autostart_);
|
|
ui_->main->set_daemon_enabled(enable_daemon_);
|
|
ui_->main->set_minimize_to_tray_enabled(enable_minimize_to_tray_);
|
|
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
|
|
ui_->main->set_server_host(signal_server_ip_self_);
|
|
ui_->main->set_server_port(signal_server_port_self_);
|
|
ui_->main->set_coturn_port(coturn_server_port_self_);
|
|
}
|
|
|
|
void GuiApplication::BindMainCallbacks() {
|
|
auto &main = ui_->main;
|
|
main->window().on_close_requested([this] {
|
|
if (MinimizeMainWindowToTray()) {
|
|
// MinimizeMainWindowToTray() ensures the tray icon exists. Let Slint
|
|
// complete the close request by hiding the main window as well.
|
|
return slint::CloseRequestResponse::HideWindow;
|
|
}
|
|
exit_ = true;
|
|
return slint::CloseRequestResponse::HideWindow;
|
|
});
|
|
main->on_copy_local_id([this] {
|
|
if (!SDL_SetClipboardText(client_id_)) {
|
|
LOG_WARN("Copy local id failed: {}", SDL_GetError());
|
|
}
|
|
});
|
|
main->on_toggle_password_visibility([this] {
|
|
show_password_ = !show_password_;
|
|
ui_->main->set_password_visible(show_password_);
|
|
});
|
|
main->on_reset_password([this](slint::SharedString value) {
|
|
const std::string password(value);
|
|
if (password.size() != 6) {
|
|
return false;
|
|
}
|
|
std::memset(password_saved_, 0, sizeof(password_saved_));
|
|
std::strncpy(password_saved_, password.c_str(),
|
|
sizeof(password_saved_) - 1);
|
|
|
|
if (config_center_->IsSelfHosted()) {
|
|
std::string identity = self_hosted_id_;
|
|
if (const auto at = identity.find('@'); at != std::string::npos) {
|
|
identity.resize(at);
|
|
}
|
|
if (identity.empty()) {
|
|
identity = client_id_;
|
|
}
|
|
identity += "@" + password;
|
|
std::memset(self_hosted_id_, 0, sizeof(self_hosted_id_));
|
|
std::strncpy(self_hosted_id_, identity.c_str(),
|
|
sizeof(self_hosted_id_) - 1);
|
|
} else {
|
|
const std::string identity = std::string(client_id_) + "@" + password;
|
|
std::memset(client_id_with_password_, 0,
|
|
sizeof(client_id_with_password_));
|
|
std::strncpy(client_id_with_password_, identity.c_str(),
|
|
sizeof(client_id_with_password_) - 1);
|
|
}
|
|
settings_.Save();
|
|
if (peer_) {
|
|
LeaveConnection(peer_, client_id_);
|
|
DestroyPeer(&peer_);
|
|
}
|
|
return true;
|
|
});
|
|
main->on_connect_requested([this](slint::SharedString id) {
|
|
ConnectFromUi(std::string(id));
|
|
});
|
|
main->on_format_remote_id([](slint::SharedString value) {
|
|
return UiText(FormatRemotePeerIdInput(std::string_view(value)));
|
|
});
|
|
main->on_recent_connect([this](slint::SharedString id) {
|
|
ConnectFromUi(std::string(id));
|
|
});
|
|
main->on_recent_edit_alias(
|
|
[this](slint::SharedString id, slint::SharedString alias) {
|
|
const std::string remote_id(id);
|
|
const std::string trimmed = Trim(std::string(alias));
|
|
if (trimmed.empty()) {
|
|
settings_.EraseRecentConnectionAlias(remote_id);
|
|
} else {
|
|
settings_.SetRecentConnectionAlias(remote_id, trimmed);
|
|
}
|
|
settings_.SaveRecentConnectionAliases();
|
|
reload_recent_connections_ = true;
|
|
});
|
|
main->on_recent_delete([this](slint::SharedString id) {
|
|
const std::string remote_id(id);
|
|
const auto connection = std::find_if(
|
|
recent_connections_.begin(), recent_connections_.end(),
|
|
[&](const auto &entry) { return entry.second.remote_id == remote_id; });
|
|
if (connection != recent_connections_.end()) {
|
|
thumbnail_->DeleteThumbnail(connection->first);
|
|
settings_.EraseRecentConnectionAlias(remote_id);
|
|
settings_.SaveRecentConnectionAliases();
|
|
reload_recent_connections_ = true;
|
|
}
|
|
});
|
|
main->on_browse_save_path([this] {
|
|
const char *folder = tinyfd_selectFolderDialog(
|
|
localization::file_transfer_save_path[localization_language_index_]
|
|
.c_str(),
|
|
file_transfer_save_path_buf_[0] ? file_transfer_save_path_buf_ : nullptr);
|
|
if (folder) {
|
|
ui_->main->set_file_save_path(folder);
|
|
}
|
|
});
|
|
main->on_save_settings([this] { SaveSettingsFromUi(); });
|
|
main->on_cancel_settings([this] { ResetSettingsUi(); });
|
|
main->on_save_self_hosted_settings([this] {
|
|
const std::string host(ui_->main->get_server_host());
|
|
const auto signal_port = ParsePort(ui_->main->get_server_port());
|
|
const auto coturn_port = ParsePort(ui_->main->get_coturn_port());
|
|
if (!host.empty()) {
|
|
config_center_->SetServerHost(host);
|
|
std::memset(signal_server_ip_self_, 0, sizeof(signal_server_ip_self_));
|
|
std::strncpy(signal_server_ip_self_, host.c_str(),
|
|
sizeof(signal_server_ip_self_) - 1);
|
|
std::memset(signal_server_ip_, 0, sizeof(signal_server_ip_));
|
|
std::strncpy(signal_server_ip_, host.c_str(),
|
|
sizeof(signal_server_ip_) - 1);
|
|
}
|
|
if (signal_port) {
|
|
config_center_->SetServerPort(*signal_port);
|
|
std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_),
|
|
"%d", *signal_port);
|
|
std::snprintf(signal_server_port_, sizeof(signal_server_port_), "%d",
|
|
*signal_port);
|
|
}
|
|
if (coturn_port) {
|
|
config_center_->SetCoturnServerPort(*coturn_port);
|
|
std::snprintf(coturn_server_port_self_,
|
|
sizeof(coturn_server_port_self_), "%d", *coturn_port);
|
|
std::snprintf(coturn_server_port_, sizeof(coturn_server_port_), "%d",
|
|
*coturn_port);
|
|
}
|
|
});
|
|
main->on_cancel_self_hosted_settings([this] {
|
|
const std::string host = config_center_->GetSignalServerHost();
|
|
ui_->main->set_server_host(UiText(host));
|
|
const int signal_port = config_center_->GetSignalServerPort();
|
|
const int coturn_port = config_center_->GetCoturnServerPort();
|
|
ui_->main->set_server_port(
|
|
signal_port > 0 ? UiText(std::to_string(signal_port))
|
|
: slint::SharedString{});
|
|
ui_->main->set_coturn_port(
|
|
coturn_port > 0 ? UiText(std::to_string(coturn_port))
|
|
: slint::SharedString{});
|
|
});
|
|
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
|
|
main->on_connection_cancel([this] {
|
|
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
|
|
if (!props) {
|
|
show_connection_status_window_ = false;
|
|
return;
|
|
}
|
|
const ConnectionStatus status = props->connection_status_.load();
|
|
if (status == ConnectionStatus::IncorrectPassword) {
|
|
std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
|
|
password_validating_ = false;
|
|
} else {
|
|
CloseStreamTab(props->remote_id_);
|
|
re_enter_remote_id_ = true;
|
|
}
|
|
show_connection_status_window_ = false;
|
|
});
|
|
main->on_connection_acknowledge([this] {
|
|
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
|
|
if (props) {
|
|
const ConnectionStatus status = props->connection_status_.load();
|
|
if (status == ConnectionStatus::NoSuchTransmissionId ||
|
|
status == ConnectionStatus::RemoteUnavailable) {
|
|
CloseStreamTab(props->remote_id_);
|
|
re_enter_remote_id_ = true;
|
|
}
|
|
}
|
|
show_connection_status_window_ = false;
|
|
});
|
|
main->on_connection_submit_password(
|
|
[this](slint::SharedString value, bool remember) {
|
|
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
|
|
const std::string password(value);
|
|
if (!props || password.empty() || password.size() > 6) {
|
|
return;
|
|
}
|
|
std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
|
|
std::memcpy(props->remote_password_, password.data(), password.size());
|
|
props->remember_password_ = remember;
|
|
props->rejoin_ = true;
|
|
password_validating_ = true;
|
|
need_to_rejoin_ = true;
|
|
});
|
|
main->on_request_screen_recording_permission([this] {
|
|
#ifdef __APPLE__
|
|
OpenScreenRecordingPreferences();
|
|
mac_screen_recording_permission_requested_ = true;
|
|
RefreshMacPermissionStatus(true);
|
|
#endif
|
|
});
|
|
main->on_request_accessibility_permission([this] {
|
|
#ifdef __APPLE__
|
|
OpenAccessibilityPreferences();
|
|
mac_accessibility_permission_requested_ = true;
|
|
RefreshMacPermissionStatus(true);
|
|
#endif
|
|
});
|
|
main->on_install_portable_service([this] {
|
|
#if _WIN32 && CROSSDESK_PORTABLE
|
|
portable_service_do_not_remind_ = false;
|
|
ui_->main->set_portable_service_do_not_remind(false);
|
|
StartPortableWindowsServiceInstall();
|
|
#endif
|
|
});
|
|
main->on_cancel_portable_service([this] {
|
|
#if _WIN32 && CROSSDESK_PORTABLE
|
|
if (portable_service_install_state_.load(std::memory_order_acquire) ==
|
|
PortableServiceInstallState::installing) {
|
|
return;
|
|
}
|
|
portable_service_do_not_remind_ =
|
|
ui_->main->get_portable_service_do_not_remind();
|
|
if (portable_service_do_not_remind_) {
|
|
portable_service_prompt_suppressed_ = true;
|
|
config_center_->SetPortableServicePromptSuppressed(true);
|
|
show_portable_service_prompt_suppressed_window_ = true;
|
|
}
|
|
show_portable_service_install_window_ = false;
|
|
#endif
|
|
});
|
|
main->on_acknowledge_portable_service([this] {
|
|
#if _WIN32 && CROSSDESK_PORTABLE
|
|
show_portable_service_install_window_ = false;
|
|
portable_service_installed_ =
|
|
portable_service_install_state_.load(std::memory_order_acquire) ==
|
|
PortableServiceInstallState::succeeded;
|
|
JoinPortableWindowsServiceInstallThread();
|
|
#endif
|
|
});
|
|
main->on_acknowledge_portable_service_suppressed([this] {
|
|
#if _WIN32 && CROSSDESK_PORTABLE
|
|
show_portable_service_prompt_suppressed_window_ = false;
|
|
#endif
|
|
});
|
|
}
|
|
|
|
void GuiApplication::BindStreamCallbacks() {
|
|
if (!ui_->stream) {
|
|
return;
|
|
}
|
|
auto &stream = *ui_->stream;
|
|
stream->window().on_close_requested([this] {
|
|
std::vector<std::string> ids;
|
|
{
|
|
std::shared_lock lock(remote_sessions_mutex_);
|
|
for (const auto &[id, _] : remote_sessions_) {
|
|
ids.push_back(id);
|
|
}
|
|
}
|
|
for (const auto &id : ids) {
|
|
CloseStreamTab(id);
|
|
}
|
|
return slint::CloseRequestResponse::HideWindow;
|
|
});
|
|
stream->on_select_tab([this](int index) { SelectStreamTab(index); });
|
|
stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
|
|
ReorderStreamTab(from, drop_x, tab_width);
|
|
});
|
|
stream->on_close_tab([this](slint::SharedString id) {
|
|
CloseStreamTab(std::string(id));
|
|
});
|
|
stream->on_switch_display([this](int index) {
|
|
auto props = SelectedSession();
|
|
if (!props || index < 0 ||
|
|
index >= static_cast<int>(props->display_info_list_.size())) {
|
|
return;
|
|
}
|
|
props->selected_display_ = index;
|
|
RemoteAction action{};
|
|
action.type = ControlType::display_id;
|
|
action.d = index;
|
|
const std::string message = action.to_json();
|
|
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
|
props->control_data_label_.c_str());
|
|
});
|
|
stream->on_send_shortcut([this](slint::SharedString shortcut) {
|
|
auto props = SelectedSession();
|
|
if (!props || !props->peer_) {
|
|
return;
|
|
}
|
|
RemoteAction action{};
|
|
action.type = ControlType::service_command;
|
|
action.c.flag = std::string(shortcut) == "Ctrl+Alt+Del"
|
|
? ServiceCommandFlag::send_sas
|
|
: ServiceCommandFlag::lock_workstation;
|
|
const std::string message = action.to_json();
|
|
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
|
props->control_data_label_.c_str());
|
|
});
|
|
stream->on_toggle_mouse_control([this] {
|
|
auto props = SelectedSession();
|
|
if (!props || !props->connection_established_) {
|
|
return;
|
|
}
|
|
props->control_mouse_ = !props->control_mouse_;
|
|
props->enable_mouse_control_ = props->control_mouse_;
|
|
start_keyboard_capturer_ = props->control_mouse_;
|
|
if (!props->control_mouse_) {
|
|
keyboard_.ForceReleasePressedKeys();
|
|
}
|
|
});
|
|
stream->on_toggle_audio([this] {
|
|
auto props = SelectedSession();
|
|
if (!props || !props->connection_established_) {
|
|
return;
|
|
}
|
|
props->audio_capture_button_pressed_ =
|
|
!props->audio_capture_button_pressed_;
|
|
RemoteAction action{};
|
|
action.type = ControlType::audio_capture;
|
|
action.a = props->audio_capture_button_pressed_;
|
|
const std::string message = action.to_json();
|
|
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
|
props->control_data_label_.c_str());
|
|
});
|
|
stream->on_select_file([this] {
|
|
auto props = SelectedSession();
|
|
if (!props) {
|
|
return;
|
|
}
|
|
transfers_.ProcessSelectedFile(
|
|
OpenFileDialog(localization::select_file[localization_language_index_]),
|
|
props, props->file_label_);
|
|
});
|
|
stream->on_close_file_transfer([this] {
|
|
if (auto props = SelectedSession()) {
|
|
props->file_transfer_.file_transfer_window_visible_ = false;
|
|
}
|
|
});
|
|
stream->on_can_drop_file([](const slint::DataTransfer &data) {
|
|
return data.has_plain_text();
|
|
});
|
|
stream->on_file_dropped([this](const slint::DataTransfer &data) {
|
|
const auto text = data.plain_text();
|
|
auto props = SelectedSession();
|
|
if (!text || !props) {
|
|
return;
|
|
}
|
|
const std::string path = DecodeDroppedPath(std::string(*text));
|
|
if (!path.empty()) {
|
|
transfers_.ProcessSelectedFile(path, props, props->file_label_,
|
|
props->remote_id_);
|
|
}
|
|
});
|
|
stream->on_toggle_stats([this] {
|
|
if (auto props = SelectedSession()) {
|
|
props->net_traffic_stats_button_pressed_ =
|
|
!props->net_traffic_stats_button_pressed_;
|
|
}
|
|
});
|
|
stream->on_toggle_fullscreen([this] {
|
|
auto &stream = *ui_->stream;
|
|
#if defined(__APPLE__)
|
|
const bool enter_fullscreen = !IsStreamWindowFullscreen();
|
|
// Update Slint's fullscreen layout before AppKit applies the new frame so
|
|
// the first fullscreen draw cannot briefly contain the windowed tab strip.
|
|
stream->set_fullscreen_enabled(enter_fullscreen);
|
|
if (!SetStreamWindowFullscreen(enter_fullscreen)) {
|
|
stream->set_fullscreen_enabled(!enter_fullscreen);
|
|
return;
|
|
}
|
|
#else
|
|
auto &window = stream->window();
|
|
const bool enter_fullscreen = !window.is_fullscreen();
|
|
#endif
|
|
fullscreen_button_pressed_ = enter_fullscreen;
|
|
|
|
#if !defined(__APPLE__)
|
|
stream->set_fullscreen_enabled(enter_fullscreen);
|
|
window.request_redraw();
|
|
window.set_fullscreen(enter_fullscreen);
|
|
#endif
|
|
});
|
|
stream->on_disconnect([this] {
|
|
if (auto props = SelectedSession()) {
|
|
CloseStreamTab(props->remote_id_);
|
|
}
|
|
});
|
|
stream->on_pointer_input([this](slint::PointerEventButton button,
|
|
slint::PointerEventKind kind, float x,
|
|
float y) {
|
|
SendPointerInput(static_cast<int>(button), static_cast<int>(kind), x, y);
|
|
});
|
|
stream->on_scroll_input([this](float dx, float dy, float x, float y) {
|
|
SendScrollInput(dx, dy, x, y);
|
|
});
|
|
stream->on_key_input(
|
|
[this](slint::SharedString text, bool pressed, bool control, bool alt,
|
|
bool shift, bool meta) {
|
|
SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
|
|
});
|
|
}
|
|
|
|
void GuiApplication::BindServerCallbacks() {
|
|
if (!ui_->server) {
|
|
return;
|
|
}
|
|
auto &server = *ui_->server;
|
|
server->on_title_drag([this](int phase, float x, float y) {
|
|
auto &drag = ui_->server_drag;
|
|
DragWindow(*ui_->server, phase, x, y, drag);
|
|
});
|
|
server->on_toggle_collapsed([this](bool collapsed) {
|
|
server_window_collapsed_ = collapsed;
|
|
const int language = (*ui_->server)->get_language_index();
|
|
const float width = ServerWindowLogicalWidth(language);
|
|
(*ui_->server)->window().set_size(
|
|
slint::LogicalSize(slint::Size<float>{
|
|
width, ServerWindowLogicalHeight(collapsed)}));
|
|
});
|
|
server->on_controller_selected([this](int index) {
|
|
if (index < 0 || index >= static_cast<int>(ui_->controller_ids.size())) {
|
|
return;
|
|
}
|
|
selected_server_remote_id_ = ui_->controller_ids[index];
|
|
});
|
|
server->on_select_file([this] {
|
|
if (selected_server_remote_id_.empty()) {
|
|
return;
|
|
}
|
|
transfers_.ProcessSelectedFile(
|
|
OpenFileDialog(localization::select_file[localization_language_index_]),
|
|
nullptr, file_label_, selected_server_remote_id_);
|
|
});
|
|
server->on_disconnect_controller([this] {
|
|
if (peer_ && !selected_server_remote_id_.empty()) {
|
|
LeaveConnection(peer_, selected_server_remote_id_.c_str());
|
|
}
|
|
});
|
|
}
|
|
|
|
void GuiApplication::Tick() {
|
|
if (exit_) {
|
|
slint::quit_event_loop();
|
|
return;
|
|
}
|
|
if (!peer_) {
|
|
CreateConnectionPeer();
|
|
}
|
|
|
|
SDL_Event event;
|
|
#if defined(__APPLE__)
|
|
// Slint's winit backend owns the macOS AppKit event loop. SDL_PollEvent()
|
|
// implicitly pumps Cocoa events, so calling it from this Slint timer would
|
|
// re-enter winit's event handler and abort the process. Only drain events
|
|
// that are already in SDL's queue; clipboard changes are polled below and
|
|
// tray/window actions use direct Slint callbacks on macOS.
|
|
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST,
|
|
SDL_EVENT_LAST) > 0) {
|
|
#else
|
|
while (SDL_PollEvent(&event)) {
|
|
#endif
|
|
if (event.type == SDL_EVENT_QUIT) {
|
|
exit_ = true;
|
|
} else if (event.type == SDL_EVENT_CLIPBOARD_UPDATE) {
|
|
clipboard_.HandleLocalUpdate();
|
|
}
|
|
}
|
|
if (ui_->capture_mode &&
|
|
(ui_->capture_page == "stream" || ui_->capture_page == "server")) {
|
|
return;
|
|
}
|
|
clipboard_.ApplyPendingRemoteText();
|
|
#if defined(__linux__) && !defined(__APPLE__)
|
|
if (ui_->tray) {
|
|
ui_->tray->ProcessEvents();
|
|
}
|
|
#endif
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (now - ui_->last_clipboard_poll >= 250ms) {
|
|
clipboard_.HandleLocalUpdate();
|
|
ui_->last_clipboard_poll = now;
|
|
}
|
|
|
|
if (need_to_rejoin_ && now - last_rejoin_check_time_ >= 1s) {
|
|
last_rejoin_check_time_ = now;
|
|
need_to_rejoin_ = false;
|
|
for (const auto &[_, props] : remote_sessions_) {
|
|
if (props && props->rejoin_) {
|
|
ConnectTo(props->remote_id_, props->remote_password_,
|
|
props->remember_password_);
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateLabels();
|
|
const bool was_reloading_recent_connections = reload_recent_connections_;
|
|
HandleRecentConnections();
|
|
if (was_reloading_recent_connections && !reload_recent_connections_) {
|
|
ui_->recent_model_signature.clear();
|
|
}
|
|
HandleConnectionStatusChange();
|
|
HandlePendingPresenceProbe();
|
|
HandleConnectionTimeouts();
|
|
HandleWindowsServiceIntegration();
|
|
devices_.UpdateInteractions();
|
|
|
|
UpdateLocalization();
|
|
SyncMainWindow();
|
|
SyncConnectionDialog();
|
|
SyncPlatformDialogs();
|
|
SyncStreamWindow();
|
|
SyncServerWindow();
|
|
}
|
|
|
|
void GuiApplication::UpdateLocalization() {
|
|
const int language =
|
|
localization::detail::ClampLanguageIndex(localization_language_index_);
|
|
if (!ui_ || ui_->localized_language == language) {
|
|
return;
|
|
}
|
|
auto &strings = ui_->main->global<ui::UiStrings>();
|
|
strings.set_local_desktop(UiText(localization::local_desktop[language]));
|
|
strings.set_remote_desktop(UiText(localization::remote_desktop[language]));
|
|
strings.set_recent_connections(
|
|
UiText(localization::recent_connections[language]));
|
|
strings.set_local_id(UiText(localization::local_id[language]));
|
|
strings.set_device_name(UiText(localization::device_name[language]));
|
|
strings.set_remote_id(UiText(localization::remote_id[language]));
|
|
strings.set_online(UiText(localization::online[language]));
|
|
strings.set_offline(UiText(localization::offline[language]));
|
|
strings.set_password(UiText(localization::password[language]));
|
|
strings.set_copied(
|
|
UiText(localization::local_id_copied_to_clipboard[language]));
|
|
strings.set_connect(UiText(localization::connect[language]));
|
|
strings.set_settings(UiText(localization::settings[language]));
|
|
strings.set_about(UiText(localization::about[language]));
|
|
strings.set_ok(UiText(localization::ok[language]));
|
|
strings.set_cancel(UiText(localization::cancel[language]));
|
|
strings.set_new_password(UiText(localization::new_password[language]));
|
|
strings.set_invalid_password(UiText(localization::max_password_len[language]));
|
|
strings.set_edit_alias(
|
|
UiText(localization::input_connection_alias[language]));
|
|
strings.set_delete_connection(
|
|
UiText(localization::delete_connection[language]));
|
|
strings.set_confirm_delete(
|
|
UiText(localization::confirm_delete_connection[language]));
|
|
strings.set_language(UiText(localization::language[language]));
|
|
strings.set_video_quality(UiText(localization::video_quality[language]));
|
|
strings.set_frame_rate(UiText(localization::video_frame_rate[language]));
|
|
strings.set_codec(UiText(localization::video_encode_format[language]));
|
|
strings.set_hardware_codec(
|
|
UiText(localization::enable_hardware_video_codec[language]));
|
|
strings.set_turn_relay(UiText(localization::enable_turn[language]));
|
|
strings.set_srtp(UiText(localization::enable_srtp[language]));
|
|
strings.set_self_hosted(
|
|
UiText(localization::self_hosted_server_config[language]));
|
|
strings.set_autostart(UiText(localization::enable_autostart[language]));
|
|
strings.set_daemon(UiText(localization::enable_daemon[language]));
|
|
strings.set_minimize_to_tray(
|
|
UiText(localization::minimize_to_tray[language]));
|
|
strings.set_file_save_path(
|
|
UiText(localization::file_transfer_save_path[language]));
|
|
strings.set_default_desktop(UiText(localization::default_desktop[language]));
|
|
strings.set_server_host(
|
|
UiText(localization::self_hosted_server_address[language]));
|
|
strings.set_server_port(
|
|
UiText(localization::self_hosted_server_port[language]));
|
|
strings.set_coturn_port(
|
|
UiText(localization::self_hosted_server_coturn_server_port[language]));
|
|
strings.set_version(UiText(localization::version[language]));
|
|
strings.set_signal_connected(
|
|
UiText(localization::signal_connected[language]));
|
|
strings.set_signal_disconnected(
|
|
UiText(localization::signal_disconnected[language]));
|
|
strings.set_tls_error(UiText(localization::signal_tls_cert_error[language]));
|
|
strings.set_update_available(
|
|
UiText(localization::new_version_available[language]));
|
|
strings.set_download(UiText(localization::update[language]));
|
|
strings.set_input_password(UiText(localization::input_password[language]));
|
|
strings.set_reinput_password(
|
|
UiText(localization::reinput_password[language]));
|
|
strings.set_remember_password(
|
|
UiText(localization::remember_password[language]));
|
|
strings.set_validate_password(
|
|
UiText(localization::validate_password[language]));
|
|
strings.set_request_permissions(
|
|
UiText(localization::request_permissions[language]));
|
|
strings.set_permission_required(
|
|
UiText(localization::permission_required_message[language]));
|
|
strings.set_screen_recording_permission(
|
|
UiText(localization::screen_recording_permission[language]));
|
|
strings.set_accessibility_permission(
|
|
UiText(localization::accessibility_permission[language]));
|
|
strings.set_service_setup_title(
|
|
UiText(localization::windows_service_setup_title[language]));
|
|
strings.set_service_setup_message(
|
|
UiText(localization::windows_service_setup_message[language]));
|
|
strings.set_service_settings_label(
|
|
UiText(localization::windows_service_settings_label[language]));
|
|
strings.set_install_service(
|
|
UiText(localization::install_windows_service[language]));
|
|
strings.set_service_installed(
|
|
UiText(localization::windows_service_installed[language]));
|
|
strings.set_do_not_remind(
|
|
UiText(localization::do_not_remind_again[language]));
|
|
strings.set_notification(UiText(localization::notification[language]));
|
|
strings.set_service_suppressed_message(
|
|
UiText(localization::windows_service_prompt_suppressed_message[language]));
|
|
strings.set_quality_low(UiText(localization::video_quality_low[language]));
|
|
strings.set_quality_medium(
|
|
UiText(localization::video_quality_medium[language]));
|
|
strings.set_quality_high(UiText(localization::video_quality_high[language]));
|
|
strings.set_codec_h264(UiText(localization::h264[language]));
|
|
strings.set_codec_av1(UiText(localization::av1[language]));
|
|
strings.set_self_hosted_settings(
|
|
UiText(localization::self_hosted_server_settings[language]));
|
|
strings.set_access_website(UiText(localization::access_website[language]));
|
|
strings.set_release_date_label(UiText(localization::release_date[language]));
|
|
strings.set_later(UiText(localization::cancel[language]));
|
|
const auto apply_stream_strings = [language](auto &stream_strings) {
|
|
stream_strings.set_select_display(
|
|
UiText(localization::select_display[language]));
|
|
stream_strings.set_send_shortcut(
|
|
UiText(localization::send_shortcut[language]));
|
|
stream_strings.set_control_mouse(
|
|
UiText(localization::control_mouse[language]));
|
|
stream_strings.set_release_mouse(
|
|
UiText(localization::release_mouse[language]));
|
|
stream_strings.set_audio(UiText(localization::audio_capture[language]));
|
|
stream_strings.set_mute(UiText(localization::mute[language]));
|
|
stream_strings.set_select_file(
|
|
UiText(localization::select_file[language]));
|
|
stream_strings.set_show_stats(
|
|
UiText(localization::show_net_traffic_stats[language]));
|
|
stream_strings.set_hide_stats(
|
|
UiText(localization::hide_net_traffic_stats[language]));
|
|
stream_strings.set_fullscreen(UiText(localization::fullscreen[language]));
|
|
stream_strings.set_exit_fullscreen(
|
|
UiText(localization::exit_fullscreen[language]));
|
|
stream_strings.set_disconnect(
|
|
UiText(localization::disconnect[language]));
|
|
stream_strings.set_file_transfer(
|
|
UiText(localization::file_transfer_progress[language]));
|
|
stream_strings.set_expand_control(
|
|
UiText(localization::expand_control_bar[language]));
|
|
stream_strings.set_collapse_control(
|
|
UiText(localization::collapse_control_bar[language]));
|
|
stream_strings.set_stats_in(UiText(localization::in[language]));
|
|
stream_strings.set_stats_out(UiText(localization::out[language]));
|
|
stream_strings.set_stats_loss_rate(
|
|
UiText(localization::loss_rate[language]));
|
|
stream_strings.set_stats_resolution(
|
|
UiText(localization::resolution[language]));
|
|
stream_strings.set_stats_connection_mode(
|
|
UiText(localization::connection_mode[language]));
|
|
};
|
|
auto &main_stream_strings = ui_->main->global<ui::StreamStrings>();
|
|
apply_stream_strings(main_stream_strings);
|
|
if (ui_->stream) {
|
|
auto &active_stream_strings =
|
|
(*ui_->stream)->global<ui::StreamStrings>();
|
|
apply_stream_strings(active_stream_strings);
|
|
}
|
|
ui_->localized_language = language;
|
|
}
|
|
|
|
void GuiApplication::SyncMainWindow() {
|
|
if (!ui_) {
|
|
return;
|
|
}
|
|
#if defined(__APPLE__)
|
|
if (ui_->main_native_titlebar_attempts > 0) {
|
|
if (HideDisabledMainWindowZoomButton()) {
|
|
ui_->main_native_titlebar_attempts = 0;
|
|
} else {
|
|
--ui_->main_native_titlebar_attempts;
|
|
}
|
|
}
|
|
#endif
|
|
if (re_enter_remote_id_) {
|
|
ui_->main->invoke_reset_remote_id();
|
|
re_enter_remote_id_ = false;
|
|
}
|
|
ui_->main->set_local_id(UiText(FormatPeerId(client_id_)));
|
|
ui_->main->set_local_password(password_saved_);
|
|
ui_->main->set_password_visible(show_password_);
|
|
ui_->main->set_signal_connected(signal_connected_);
|
|
ui_->main->set_signal_tls_error(
|
|
signal_status_ == SignalStatus::SignalTlsCertError);
|
|
ui_->main->set_update_available(update_available_);
|
|
ui_->main->set_current_version(CROSSDESK_VERSION);
|
|
ui_->main->set_latest_version(UiText(latest_version_));
|
|
ui_->main->set_release_name(UiText(release_name_));
|
|
ui_->main->set_release_notes(UiText(CleanReleaseNotesForUi(release_notes_)));
|
|
ui_->main->set_release_date(UiText(release_date_));
|
|
ui_->main->set_settings_session_active(stream_window_inited_);
|
|
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
|
!defined(__arm__) && USE_CUDA) || \
|
|
defined(__APPLE__))
|
|
ui_->main->set_hardware_codec_visible(true);
|
|
#else
|
|
ui_->main->set_hardware_codec_visible(false);
|
|
#endif
|
|
|
|
if (show_offline_warning_window_) {
|
|
ui_->main->set_offline_warning(UiText(offline_warning_text_));
|
|
show_offline_warning_window_ = false;
|
|
}
|
|
|
|
std::ostringstream signature_builder;
|
|
for (const auto &[key, connection] : recent_connections_) {
|
|
signature_builder << key << '\0' << connection.remote_id << '\0'
|
|
<< connection.remote_host_name << '\0'
|
|
<< settings_.RecentConnectionDisplayName(connection)
|
|
<< '\0'
|
|
<< device_presence_cache_.IsOnline(connection.remote_id)
|
|
<< '\n';
|
|
}
|
|
const std::string signature = signature_builder.str();
|
|
if (ui_->recent_model_signature == signature) {
|
|
return;
|
|
}
|
|
ui_->recent_model_signature = signature;
|
|
|
|
std::vector<ui::RecentConnection> model;
|
|
model.reserve(recent_connections_.size());
|
|
for (const auto &[_, connection] : recent_connections_) {
|
|
ui::RecentConnection item;
|
|
item.remote_id = UiText(connection.remote_id);
|
|
item.host_name = UiText(connection.remote_host_name);
|
|
item.display_name =
|
|
UiText(settings_.RecentConnectionDisplayName(connection));
|
|
item.online = device_presence_cache_.IsOnline(connection.remote_id);
|
|
std::vector<unsigned char> rgba;
|
|
int image_width = 0;
|
|
int image_height = 0;
|
|
if (!connection.image_path.empty() &&
|
|
thumbnail_->DecodeImage(connection.image_path, &rgba, &image_width,
|
|
&image_height)) {
|
|
slint::SharedPixelBuffer<slint::Rgba8Pixel> pixels(image_width,
|
|
image_height);
|
|
std::memcpy(pixels.begin(), rgba.data(), rgba.size());
|
|
item.thumbnail = slint::Image(std::move(pixels));
|
|
}
|
|
model.push_back(std::move(item));
|
|
}
|
|
ui_->recent_model->set_vector(std::move(model));
|
|
}
|
|
|
|
void GuiApplication::SyncConnectionDialog() {
|
|
#ifdef CROSSDESK_DEBUG
|
|
if (ui_ && ui_->capture_mode &&
|
|
(ui_->capture_page == "connection" ||
|
|
ui_->capture_page == "connection-password")) {
|
|
return;
|
|
}
|
|
#endif
|
|
if (!ui_ || !show_connection_status_window_) {
|
|
if (ui_) {
|
|
ui_->main->set_connection_dialog_open(false);
|
|
ui_->connection_password_initialized = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
auto props = FindRemoteSession(focused_remote_id_);
|
|
if (!props) {
|
|
ui_->main->set_connection_dialog_open(false);
|
|
return;
|
|
}
|
|
|
|
if (ui_->connection_dialog_remote_id != props->remote_id_) {
|
|
ui_->connection_password_initialized = false;
|
|
}
|
|
ui_->connection_dialog_remote_id = props->remote_id_;
|
|
const ConnectionStatus status = props->connection_status_.load();
|
|
const bool password_required = status == ConnectionStatus::IncorrectPassword;
|
|
const bool pending = status == ConnectionStatus::Connecting ||
|
|
status == ConnectionStatus::Gathering;
|
|
std::string text;
|
|
if (password_required) {
|
|
text = password_validating_
|
|
? localization::validate_password[localization_language_index_]
|
|
: password_validating_time_ <= 1
|
|
? localization::input_password[localization_language_index_]
|
|
: localization::reinput_password[localization_language_index_];
|
|
} else {
|
|
text = ConnectionStatusText(status, localization_language_index_);
|
|
}
|
|
|
|
ui_->main->set_connection_dialog_open(true);
|
|
ui_->main->set_connection_status_text(UiText(text));
|
|
ui_->main->set_connection_pending(pending);
|
|
ui_->main->set_connection_password_required(password_required);
|
|
ui_->main->set_connection_validating(password_validating_);
|
|
if (password_required && !password_validating_ &&
|
|
!ui_->connection_password_initialized) {
|
|
ui_->main->set_connection_password(props->remote_password_);
|
|
ui_->main->set_connection_remember_password(props->remember_password_);
|
|
ui_->connection_password_initialized = true;
|
|
} else if (!password_required) {
|
|
ui_->connection_password_initialized = false;
|
|
}
|
|
}
|
|
|
|
void GuiApplication::SyncPlatformDialogs() {
|
|
#ifdef CROSSDESK_DEBUG
|
|
if (ui_ && ui_->capture_mode &&
|
|
(ui_->capture_page == "service" ||
|
|
ui_->capture_page == "service-notice")) {
|
|
return;
|
|
}
|
|
#endif
|
|
#if _WIN32 && CROSSDESK_PORTABLE
|
|
CheckPortableWindowsService();
|
|
const PortableServiceInstallState state =
|
|
portable_service_install_state_.load(std::memory_order_acquire);
|
|
if (state == PortableServiceInstallState::succeeded) {
|
|
portable_service_installed_ = true;
|
|
}
|
|
ui_->main->set_portable_service_settings_visible(true);
|
|
ui_->main->set_portable_service_dialog_open(
|
|
show_portable_service_install_window_);
|
|
ui_->main->set_portable_service_suppressed_notice_open(
|
|
show_portable_service_prompt_suppressed_window_);
|
|
ui_->main->set_portable_service_installed(portable_service_installed_);
|
|
ui_->main->set_portable_service_installing(
|
|
state == PortableServiceInstallState::installing);
|
|
ui_->main->set_portable_service_succeeded(
|
|
state == PortableServiceInstallState::succeeded);
|
|
const std::string *status = nullptr;
|
|
if (state == PortableServiceInstallState::installing) {
|
|
status = &localization::installing_windows_service
|
|
[localization_language_index_];
|
|
} else if (state == PortableServiceInstallState::succeeded) {
|
|
status = &localization::windows_service_install_success
|
|
[localization_language_index_];
|
|
} else if (state == PortableServiceInstallState::failed) {
|
|
status = &localization::windows_service_install_failed
|
|
[localization_language_index_];
|
|
}
|
|
ui_->main->set_portable_service_status(
|
|
status ? UiText(*status) : slint::SharedString{});
|
|
if (show_portable_service_install_window_) {
|
|
if (!ui_->portable_service_dialog_initialized) {
|
|
ui_->main->set_portable_service_do_not_remind(
|
|
portable_service_do_not_remind_);
|
|
ui_->portable_service_dialog_initialized = true;
|
|
}
|
|
} else {
|
|
ui_->portable_service_dialog_initialized = false;
|
|
}
|
|
#else
|
|
ui_->main->set_portable_service_settings_visible(false);
|
|
ui_->main->set_portable_service_dialog_open(false);
|
|
ui_->main->set_portable_service_suppressed_notice_open(false);
|
|
#endif
|
|
#ifdef __APPLE__
|
|
if (ui_->capture_mode) {
|
|
const bool show_permission = ui_->capture_page == "permission";
|
|
ui_->main->set_permission_dialog_open(show_permission);
|
|
ui_->main->set_screen_recording_granted(!show_permission);
|
|
ui_->main->set_accessibility_granted(!show_permission);
|
|
return;
|
|
}
|
|
RefreshMacPermissionStatus(false);
|
|
show_request_permission_window_ =
|
|
!mac_screen_recording_permission_granted_ ||
|
|
!mac_accessibility_permission_granted_;
|
|
ui_->main->set_permission_dialog_open(show_request_permission_window_);
|
|
ui_->main->set_screen_recording_granted(
|
|
mac_screen_recording_permission_granted_);
|
|
ui_->main->set_accessibility_granted(
|
|
mac_accessibility_permission_granted_);
|
|
#else
|
|
ui_->main->set_permission_dialog_open(false);
|
|
#endif
|
|
}
|
|
|
|
void GuiApplication::SyncStreamWindow() {
|
|
bool has_sessions = false;
|
|
{
|
|
std::shared_lock lock(remote_sessions_mutex_);
|
|
has_sessions = !remote_sessions_.empty();
|
|
}
|
|
if (need_to_create_stream_window_ && !ui_->stream) {
|
|
ui_->stream.emplace(ui::StreamWindow::create());
|
|
RegisterFontAwesome((*ui_->stream)->window());
|
|
// Slint globals belong to a component tree. Force the next localization
|
|
// pass to initialize the newly-created, independent stream window tree.
|
|
ui_->localized_language = -1;
|
|
(*ui_->stream)->set_tabs(ui_->tab_model);
|
|
(*ui_->stream)->set_displays(ui_->display_model);
|
|
(*ui_->stream)->set_file_transfers(ui_->transfer_model);
|
|
(*ui_->stream)->set_stats_rows(ui_->stats_model);
|
|
BindStreamCallbacks();
|
|
(*ui_->stream)->show();
|
|
// Repeat after the next layout pass because the backend resolves the
|
|
// initial DPI and preferred size asynchronously on some platforms.
|
|
ui_->stream_initial_position_attempts = 2;
|
|
#if defined(__APPLE__)
|
|
ui_->stream_live_resize_configuration_attempts = 30;
|
|
#endif
|
|
stream_window_created_ = true;
|
|
stream_window_inited_ = true;
|
|
need_to_create_stream_window_ = false;
|
|
}
|
|
if (!ui_->stream) {
|
|
return;
|
|
}
|
|
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
|
#if defined(__APPLE__)
|
|
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
|
if (ConfigureStreamWindowLiveResize()) {
|
|
ui_->stream_live_resize_configuration_attempts = 0;
|
|
} else {
|
|
--ui_->stream_live_resize_configuration_attempts;
|
|
}
|
|
}
|
|
#endif
|
|
if (ui_->stream_initial_position_attempts > 0) {
|
|
if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
|
|
kStreamWindowLogicalWidth,
|
|
kStreamWindowLogicalHeight)) {
|
|
--ui_->stream_initial_position_attempts;
|
|
} else {
|
|
ui_->stream_initial_position_attempts = 0;
|
|
}
|
|
}
|
|
if (!has_sessions) {
|
|
#if defined(__APPLE__)
|
|
SetStreamWindowFullscreen(false);
|
|
#endif
|
|
(*ui_->stream)->hide();
|
|
ui_->stream.reset();
|
|
stream_window_created_ = false;
|
|
stream_window_inited_ = false;
|
|
ui_->stream_initial_position_attempts = 0;
|
|
ui_->stream_live_resize_configuration_attempts = 0;
|
|
focused_remote_id_.clear();
|
|
controlled_remote_id_.clear();
|
|
ui_->tab_order.clear();
|
|
ui_->tab_ids.clear();
|
|
ui_->tab_model_signature.clear();
|
|
return;
|
|
}
|
|
|
|
std::vector<ui::StreamTab> tabs;
|
|
std::unordered_map<std::string, ui::StreamTab> tabs_by_id;
|
|
{
|
|
std::shared_lock lock(remote_sessions_mutex_);
|
|
tabs_by_id.reserve(remote_sessions_.size());
|
|
for (const auto &[id, props] : remote_sessions_) {
|
|
if (!props || !props->tab_opened_) {
|
|
continue;
|
|
}
|
|
ui::StreamTab tab;
|
|
tab.remote_id = UiText(id);
|
|
tab.title = UiText(props->remote_host_name_.empty()
|
|
? id
|
|
: props->remote_host_name_);
|
|
tab.connected = props->connection_status_.load() ==
|
|
ConnectionStatus::Connected;
|
|
tabs_by_id.emplace(id, std::move(tab));
|
|
}
|
|
}
|
|
std::erase_if(ui_->tab_order, [&tabs_by_id](const std::string &id) {
|
|
return !tabs_by_id.contains(id);
|
|
});
|
|
for (const auto &[id, _] : tabs_by_id) {
|
|
if (std::find(ui_->tab_order.begin(), ui_->tab_order.end(), id) ==
|
|
ui_->tab_order.end()) {
|
|
ui_->tab_order.push_back(id);
|
|
}
|
|
}
|
|
ui_->tab_ids = ui_->tab_order;
|
|
tabs.reserve(ui_->tab_ids.size());
|
|
std::ostringstream tab_signature_builder;
|
|
for (const auto &id : ui_->tab_ids) {
|
|
if (auto found = tabs_by_id.find(id); found != tabs_by_id.end()) {
|
|
tab_signature_builder << id << '\0' << std::string(found->second.title)
|
|
<< '\0' << found->second.connected << '\n';
|
|
tabs.push_back(std::move(found->second));
|
|
}
|
|
}
|
|
const std::string tab_signature = tab_signature_builder.str();
|
|
if (ui_->tab_model_signature != tab_signature) {
|
|
ui_->tab_model_signature = tab_signature;
|
|
ui_->tab_model->set_vector(std::move(tabs));
|
|
}
|
|
|
|
int selected_index = 0;
|
|
if (focused_remote_id_.empty() && !ui_->tab_ids.empty()) {
|
|
focused_remote_id_ = ui_->tab_ids.front();
|
|
}
|
|
for (int i = 0; i < static_cast<int>(ui_->tab_ids.size()); ++i) {
|
|
if (ui_->tab_ids[i] == focused_remote_id_) {
|
|
selected_index = i;
|
|
break;
|
|
}
|
|
}
|
|
(*ui_->stream)->set_selected_tab(selected_index);
|
|
SelectStreamTab(selected_index);
|
|
|
|
auto props = SelectedSession();
|
|
if (!props) {
|
|
return;
|
|
}
|
|
const ConnectionStatus status = props->connection_status_.load();
|
|
// Once the peer is connected, the frame-waiting hint replaces the
|
|
// connection status. Keeping these states mutually exclusive also avoids a
|
|
// transient overlap when both properties are synchronized in one tick.
|
|
(*ui_->stream)->set_status_text(UiText(
|
|
status == ConnectionStatus::Connected
|
|
? std::string{}
|
|
: ConnectionStatusText(status, localization_language_index_)));
|
|
(*ui_->stream)->set_receiving_text(
|
|
UiText(status == ConnectionStatus::Connected
|
|
? localization::receiving_screen[localization_language_index_]
|
|
: std::string{}));
|
|
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
|
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
|
|
#if defined(__APPLE__)
|
|
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
|
|
#else
|
|
fullscreen_button_pressed_ = (*ui_->stream)->window().is_fullscreen();
|
|
#endif
|
|
(*ui_->stream)->set_fullscreen_enabled(fullscreen_button_pressed_);
|
|
(*ui_->stream)->set_stats_visible(
|
|
props->net_traffic_stats_button_pressed_);
|
|
|
|
std::vector<slint::SharedString> displays;
|
|
displays.reserve(props->display_info_list_.size());
|
|
for (const auto &display : props->display_info_list_) {
|
|
displays.emplace_back(UiText(display.name));
|
|
}
|
|
ui_->display_model->set_vector(std::move(displays));
|
|
(*ui_->stream)->set_selected_display(props->selected_display_);
|
|
|
|
const auto &net = props->net_traffic_stats_;
|
|
std::vector<ui::NetworkStatsRow> stats_rows;
|
|
stats_rows.reserve(4);
|
|
const auto append_stats_row = [&](const std::string &label,
|
|
const auto &inbound,
|
|
const auto &outbound) {
|
|
ui::NetworkStatsRow row;
|
|
row.label = UiText(label);
|
|
row.inbound = UiText(FormatBitrate(inbound.bitrate));
|
|
row.outbound = UiText(FormatBitrate(outbound.bitrate));
|
|
row.loss_rate = UiText(FormatLossRate(inbound.loss_rate));
|
|
stats_rows.push_back(std::move(row));
|
|
};
|
|
append_stats_row(localization::video[localization_language_index_],
|
|
net.video_inbound_stats, net.video_outbound_stats);
|
|
append_stats_row(localization::audio[localization_language_index_],
|
|
net.audio_inbound_stats, net.audio_outbound_stats);
|
|
append_stats_row(localization::data[localization_language_index_],
|
|
net.data_inbound_stats, net.data_outbound_stats);
|
|
append_stats_row(localization::total[localization_language_index_],
|
|
net.total_inbound_stats, net.total_outbound_stats);
|
|
ui_->stats_model->set_vector(std::move(stats_rows));
|
|
(*ui_->stream)->set_stats_fps(UiText(std::to_string(props->fps_)));
|
|
(*ui_->stream)->set_stats_resolution(UiText(
|
|
std::to_string(props->video_width_) + "x" +
|
|
std::to_string(props->video_height_)));
|
|
(*ui_->stream)->set_stats_connection_mode(UiText(
|
|
props->traversal_mode_ == TraversalMode::P2P
|
|
? localization::connection_mode_direct[localization_language_index_]
|
|
: localization::connection_mode_relay[localization_language_index_]));
|
|
|
|
std::vector<RemoteSession::FileTransferInfo> file_list;
|
|
{
|
|
std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
|
|
file_list = props->file_transfer_.file_transfer_list_;
|
|
}
|
|
const auto transfer_priority = [](RemoteSession::FileTransferStatus status) {
|
|
switch (status) {
|
|
case RemoteSession::FileTransferStatus::Sending:
|
|
return 0;
|
|
case RemoteSession::FileTransferStatus::Completed:
|
|
return 1;
|
|
case RemoteSession::FileTransferStatus::Queued:
|
|
return 2;
|
|
case RemoteSession::FileTransferStatus::Failed:
|
|
return 3;
|
|
}
|
|
return 3;
|
|
};
|
|
std::stable_sort(file_list.begin(), file_list.end(),
|
|
[&](const auto &left, const auto &right) {
|
|
return transfer_priority(left.status) <
|
|
transfer_priority(right.status);
|
|
});
|
|
|
|
std::vector<ui::FileTransferEntry> transfers;
|
|
transfers.reserve(file_list.size());
|
|
for (const auto &info : file_list) {
|
|
ui::FileTransferEntry item;
|
|
item.name = UiText(info.file_name);
|
|
const auto status_index = static_cast<int>(info.status);
|
|
if (status_index == 0)
|
|
item.status = UiText(localization::queued[localization_language_index_]);
|
|
if (status_index == 1)
|
|
item.status = UiText(localization::sending[localization_language_index_]);
|
|
if (status_index == 2)
|
|
item.status = UiText(localization::completed[localization_language_index_]);
|
|
if (status_index == 3)
|
|
item.status = UiText(localization::failed[localization_language_index_]);
|
|
item.progress = status_index == 2
|
|
? 1.0f
|
|
: info.file_size == 0
|
|
? 0.0f
|
|
: std::clamp(
|
|
static_cast<float>(info.sent_bytes) /
|
|
static_cast<float>(info.file_size),
|
|
0.0f, 1.0f);
|
|
item.speed = UiText(FormatTransferRate(info.rate_bps));
|
|
item.size = UiText(FormatFileSize(info.file_size));
|
|
transfers.push_back(std::move(item));
|
|
}
|
|
ui_->transfer_model->set_vector(std::move(transfers));
|
|
(*ui_->stream)->set_file_transfer_visible(
|
|
props->file_transfer_.file_transfer_window_visible_);
|
|
|
|
std::shared_ptr<std::vector<unsigned char>> frame;
|
|
int width = 0;
|
|
int height = 0;
|
|
uint64_t sequence = 0;
|
|
{
|
|
std::lock_guard lock(props->video_frame_mutex_);
|
|
frame = props->front_frame_;
|
|
width = props->video_width_;
|
|
height = props->video_height_;
|
|
sequence = props->video_frame_sequence_;
|
|
}
|
|
const size_t nv12_size = static_cast<size_t>(width) * height * 3 / 2;
|
|
if (frame && width > 0 && height > 0 && frame->size() >= nv12_size &&
|
|
ui_->displayed_frame_sequence[props->remote_id_] != sequence) {
|
|
slint::SharedPixelBuffer<slint::Rgb8Pixel> pixels(width, height);
|
|
const int result = libyuv::NV12ToRAW(
|
|
frame->data(), width, frame->data() + static_cast<size_t>(width) * height,
|
|
width, reinterpret_cast<uint8_t *>(pixels.begin()), width * 3, width,
|
|
height);
|
|
if (result == 0) {
|
|
(*ui_->stream)->set_frame(slint::Image(std::move(pixels)));
|
|
(*ui_->stream)->set_has_frame(true);
|
|
(*ui_->stream)->set_receiving_text("");
|
|
ui_->displayed_frame_sequence[props->remote_id_] = sequence;
|
|
}
|
|
} else if (!frame) {
|
|
(*ui_->stream)->set_has_frame(false);
|
|
}
|
|
}
|
|
|
|
void GuiApplication::SyncServerWindow() {
|
|
if (need_to_create_server_window_ && !ui_->server) {
|
|
ui_->server.emplace(ui::ServerWindow::create());
|
|
RegisterFontAwesome((*ui_->server)->window());
|
|
(*ui_->server)->set_controllers(ui_->controller_model);
|
|
(*ui_->server)->set_controller_names(ui_->controller_name_model);
|
|
(*ui_->server)->set_language_index(localization_language_index_);
|
|
server_window_collapsed_ = false;
|
|
(*ui_->server)->window().set_size(slint::LogicalSize(slint::Size<float>{
|
|
ServerWindowLogicalWidth(localization_language_index_),
|
|
ServerWindowLogicalHeight(false)}));
|
|
BindServerCallbacks();
|
|
(*ui_->server)->show();
|
|
// Apply once immediately and once after the next layout pass. This keeps
|
|
// the right and bottom edges correct when the backend resolves DPI and
|
|
// language-dependent preferred width asynchronously on first show.
|
|
ui_->server_initial_position_attempts = 2;
|
|
server_window_created_ = true;
|
|
server_window_inited_ = true;
|
|
need_to_create_server_window_ = false;
|
|
}
|
|
if (need_to_destroy_server_window_ && ui_->server) {
|
|
(*ui_->server)->hide();
|
|
ui_->server.reset();
|
|
server_window_created_ = false;
|
|
server_window_inited_ = false;
|
|
ui_->server_initial_position_attempts = 0;
|
|
need_to_destroy_server_window_ = false;
|
|
}
|
|
if (!ui_->server) {
|
|
return;
|
|
}
|
|
|
|
std::vector<ui::ControllerEntry> controllers;
|
|
std::vector<slint::SharedString> names;
|
|
ui_->controller_ids.clear();
|
|
{
|
|
std::shared_lock lock(connection_status_mutex_);
|
|
controllers.reserve(connection_status_.size());
|
|
for (const auto &[id, _] : connection_status_) {
|
|
const auto host = connection_host_names_.find(id);
|
|
const std::string name =
|
|
host != connection_host_names_.end() && !host->second.empty()
|
|
? host->second
|
|
: id;
|
|
ui::ControllerEntry entry;
|
|
entry.remote_id = UiText(id);
|
|
entry.display_name = UiText(name);
|
|
controllers.push_back(entry);
|
|
names.emplace_back(UiText(name));
|
|
ui_->controller_ids.push_back(id);
|
|
}
|
|
}
|
|
ui_->controller_model->set_vector(std::move(controllers));
|
|
ui_->controller_name_model->set_vector(std::move(names));
|
|
if (selected_server_remote_id_.empty() && !ui_->controller_ids.empty()) {
|
|
selected_server_remote_id_ = ui_->controller_ids.front();
|
|
}
|
|
int selected = 0;
|
|
for (int i = 0; i < static_cast<int>(ui_->controller_ids.size()); ++i) {
|
|
if (ui_->controller_ids[i] == selected_server_remote_id_) {
|
|
selected = i;
|
|
break;
|
|
}
|
|
}
|
|
(*ui_->server)->set_selected_controller(selected);
|
|
(*ui_->server)->set_language_index(localization_language_index_);
|
|
if (ui_->server_initial_position_attempts > 0) {
|
|
const float server_width =
|
|
ServerWindowLogicalWidth(localization_language_index_);
|
|
const float server_height =
|
|
ServerWindowLogicalHeight(server_window_collapsed_);
|
|
(*ui_->server)->window().set_size(slint::LogicalSize(
|
|
slint::Size<float>{server_width, server_height}));
|
|
if (PositionWindowAtBottomRight((*ui_->server)->window(), server_width,
|
|
server_height)) {
|
|
--ui_->server_initial_position_attempts;
|
|
} else {
|
|
ui_->server_initial_position_attempts = 0;
|
|
}
|
|
}
|
|
(*ui_->server)->set_controller_label(
|
|
UiText(localization::controller[localization_language_index_]));
|
|
(*ui_->server)->set_connection_label(
|
|
UiText(localization::connection_status[localization_language_index_]));
|
|
(*ui_->server)->set_file_transfer_label(
|
|
UiText(localization::file_transfer[localization_language_index_]));
|
|
(*ui_->server)->set_select_file_label(
|
|
UiText(localization::select_file[localization_language_index_]));
|
|
|
|
ConnectionStatus status = ConnectionStatus::Closed;
|
|
{
|
|
std::shared_lock lock(connection_status_mutex_);
|
|
if (const auto found = connection_status_.find(selected_server_remote_id_);
|
|
found != connection_status_.end()) {
|
|
status = found->second;
|
|
}
|
|
}
|
|
(*ui_->server)->set_connection_status(UiText(
|
|
ConnectionStatusText(status, localization_language_index_)));
|
|
|
|
auto &transfer = transfers_.global_state();
|
|
const auto sent = transfer.file_sent_bytes_.load();
|
|
const auto total = transfer.file_total_bytes_.load();
|
|
(*ui_->server)->set_file_transfer_visible(
|
|
transfer.file_transfer_window_visible_);
|
|
(*ui_->server)->set_sending_file(transfer.file_sending_.load());
|
|
(*ui_->server)->set_file_progress(
|
|
total == 0 ? 0.0f
|
|
: std::clamp(static_cast<float>(sent) /
|
|
static_cast<float>(total),
|
|
0.0f, 1.0f));
|
|
(*ui_->server)->set_file_progress_text(
|
|
UiText(FormatTransferRate(transfer.file_send_rate_bps_.load())));
|
|
std::string current_file_name;
|
|
const uint32_t current_file_id = transfer.current_file_id_.load();
|
|
if (current_file_id != 0) {
|
|
std::lock_guard lock(transfer.file_transfer_list_mutex_);
|
|
for (const auto &info : transfer.file_transfer_list_) {
|
|
if (info.file_id == current_file_id) {
|
|
current_file_name = info.file_name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
(*ui_->server)->set_current_file_name(UiText(current_file_name));
|
|
(*ui_->server)->set_file_size_text(
|
|
total == 0 ? slint::SharedString{}
|
|
: UiText(FormatFileSize(sent) + " / " +
|
|
FormatFileSize(total)));
|
|
}
|
|
|
|
void GuiApplication::SaveSettingsFromUi() {
|
|
auto &main = ui_->main;
|
|
language_button_value_ = localization::detail::ClampLanguageIndex(
|
|
main->get_language_index());
|
|
video_quality_button_value_ =
|
|
std::clamp(main->get_video_quality_index(), 0, 2);
|
|
video_frame_rate_button_value_ =
|
|
std::clamp(main->get_frame_rate_index(), 0, 1);
|
|
video_encode_format_button_value_ =
|
|
std::clamp(main->get_codec_index(), 0, 1);
|
|
enable_hardware_video_codec_ = main->get_hardware_codec_enabled();
|
|
enable_turn_ = main->get_turn_enabled();
|
|
enable_srtp_ = main->get_srtp_enabled();
|
|
enable_self_hosted_ = main->get_self_hosted_enabled();
|
|
enable_autostart_ = main->get_autostart_enabled();
|
|
enable_daemon_ = main->get_daemon_enabled();
|
|
enable_minimize_to_tray_ = main->get_minimize_to_tray_enabled();
|
|
|
|
localization_language_ =
|
|
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
|
|
localization_language_index_ = language_button_value_;
|
|
config_center_->SetLanguage(localization_language_);
|
|
config_center_->SetVideoQuality(
|
|
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
|
|
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
|
|
video_frame_rate_button_value_));
|
|
config_center_->SetVideoEncodeFormat(
|
|
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
|
|
video_encode_format_button_value_));
|
|
config_center_->SetHardwareVideoCodec(enable_hardware_video_codec_);
|
|
config_center_->SetTurn(enable_turn_);
|
|
config_center_->SetSrtp(enable_srtp_);
|
|
config_center_->SetSelfHosted(enable_self_hosted_);
|
|
config_center_->SetAutostart(enable_autostart_);
|
|
config_center_->SetDaemon(enable_daemon_);
|
|
config_center_->SetMinimizeToTray(enable_minimize_to_tray_);
|
|
|
|
const std::string path(main->get_file_save_path());
|
|
std::memset(file_transfer_save_path_buf_, 0,
|
|
sizeof(file_transfer_save_path_buf_));
|
|
std::strncpy(file_transfer_save_path_buf_, path.c_str(),
|
|
sizeof(file_transfer_save_path_buf_) - 1);
|
|
config_center_->SetFileTransferSavePath(path);
|
|
|
|
const std::string host(main->get_server_host());
|
|
if (!host.empty()) {
|
|
config_center_->SetServerHost(host);
|
|
}
|
|
if (const auto port = ParsePort(main->get_server_port())) {
|
|
config_center_->SetServerPort(*port);
|
|
}
|
|
if (const auto port = ParsePort(main->get_coturn_port())) {
|
|
config_center_->SetCoturnServerPort(*port);
|
|
}
|
|
|
|
language_button_value_last_ = language_button_value_;
|
|
video_quality_button_value_last_ = video_quality_button_value_;
|
|
video_frame_rate_button_value_last_ = video_frame_rate_button_value_;
|
|
video_encode_format_button_value_last_ =
|
|
video_encode_format_button_value_;
|
|
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
|
|
enable_turn_last_ = enable_turn_;
|
|
enable_srtp_last_ = enable_srtp_;
|
|
enable_self_hosted_last_ = enable_self_hosted_;
|
|
enable_autostart_last_ = enable_autostart_;
|
|
enable_daemon_last_ = enable_daemon_;
|
|
enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
|
|
file_transfer_save_path_last_ = path;
|
|
ui_->localized_language = -1;
|
|
|
|
if (!stream_window_inited_) {
|
|
CloseAllRemoteSessions();
|
|
CreateConnectionPeer();
|
|
}
|
|
}
|
|
|
|
void GuiApplication::ConnectFromUi(const std::string &input) {
|
|
const std::string remote_id = CompactPeerId(input);
|
|
if (remote_id.empty()) {
|
|
return;
|
|
}
|
|
if (auto existing = FindRemoteSession(remote_id);
|
|
existing && existing->connection_established_) {
|
|
focused_remote_id_ = remote_id;
|
|
need_to_create_stream_window_ = true;
|
|
return;
|
|
}
|
|
|
|
for (const auto &[_, connection] : recent_connections_) {
|
|
if (connection.remote_id == remote_id) {
|
|
ConnectTo(remote_id, connection.password.c_str(),
|
|
connection.remember_password);
|
|
return;
|
|
}
|
|
}
|
|
ConnectTo(remote_id, "", false);
|
|
}
|
|
|
|
void GuiApplication::SelectStreamTab(int index) {
|
|
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
|
|
return;
|
|
}
|
|
focused_remote_id_ = ui_->tab_ids[index];
|
|
controlled_remote_id_ = focused_remote_id_;
|
|
std::shared_lock lock(remote_sessions_mutex_);
|
|
for (auto &[id, props] : remote_sessions_) {
|
|
if (props) {
|
|
props->tab_selected_ = id == focused_remote_id_;
|
|
}
|
|
}
|
|
}
|
|
|
|
void GuiApplication::ReorderStreamTab(int from, float drop_x,
|
|
float tab_width) {
|
|
if (!ui_ || from < 0 ||
|
|
from >= static_cast<int>(ui_->tab_order.size()) || tab_width <= 0.0f) {
|
|
return;
|
|
}
|
|
const float slot_width = tab_width + 1.0f;
|
|
const int target = std::clamp(static_cast<int>(std::floor(drop_x / slot_width)),
|
|
0,
|
|
static_cast<int>(ui_->tab_order.size()) - 1);
|
|
if (target == from) {
|
|
return;
|
|
}
|
|
std::string moved = std::move(ui_->tab_order[from]);
|
|
ui_->tab_order.erase(ui_->tab_order.begin() + from);
|
|
ui_->tab_order.insert(ui_->tab_order.begin() + target, std::move(moved));
|
|
ui_->tab_ids = ui_->tab_order;
|
|
SelectStreamTab(target);
|
|
}
|
|
|
|
void GuiApplication::CloseStreamTab(const std::string &remote_id) {
|
|
std::shared_ptr<RemoteSession> props;
|
|
{
|
|
std::shared_lock lock(remote_sessions_mutex_);
|
|
if (const auto found = remote_sessions_.find(remote_id);
|
|
found != remote_sessions_.end()) {
|
|
props = found->second;
|
|
}
|
|
}
|
|
if (!props) {
|
|
return;
|
|
}
|
|
keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
|
|
CloseRemoteSession(props);
|
|
WaitForThumbnailSaveTasks();
|
|
reload_recent_connections_ = true;
|
|
recent_connection_image_save_time_ = 0;
|
|
ResetRemoteSessionResources(props);
|
|
{
|
|
std::unique_lock lock(remote_sessions_mutex_);
|
|
remote_sessions_.erase(remote_id);
|
|
}
|
|
ui_->displayed_frame_sequence.erase(remote_id);
|
|
if (focused_remote_id_ == remote_id) {
|
|
focused_remote_id_.clear();
|
|
controlled_remote_id_.clear();
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<GuiApplication::RemoteSession>
|
|
GuiApplication::SelectedSession() {
|
|
std::shared_lock lock(remote_sessions_mutex_);
|
|
auto found = remote_sessions_.find(focused_remote_id_);
|
|
if (found != remote_sessions_.end()) {
|
|
return found->second;
|
|
}
|
|
for (const auto &[_, props] : remote_sessions_) {
|
|
if (props && props->tab_selected_) {
|
|
return props;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
|
|
auto props = SelectedSession();
|
|
if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
|
|
return;
|
|
}
|
|
|
|
const auto size = (*ui_->stream)->window().size();
|
|
const float scale = (*ui_->stream)->window().scale_factor();
|
|
const float available_width = size.width / scale;
|
|
const float available_height =
|
|
size.height / scale -
|
|
(fullscreen_button_pressed_
|
|
? 0.0f
|
|
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
|
float render_width = available_width;
|
|
float render_height = available_height;
|
|
float offset_x = 0;
|
|
float offset_y = 0;
|
|
if (props->video_width_ > 0 && props->video_height_ > 0 &&
|
|
available_width > 0 && available_height > 0) {
|
|
const float video_aspect =
|
|
static_cast<float>(props->video_width_) / props->video_height_;
|
|
const float area_aspect = available_width / available_height;
|
|
if (video_aspect > area_aspect) {
|
|
render_height = available_width / video_aspect;
|
|
offset_y = (available_height - render_height) * 0.5f;
|
|
} else {
|
|
render_width = available_height * video_aspect;
|
|
offset_x = (available_width - render_width) * 0.5f;
|
|
}
|
|
}
|
|
if (x < offset_x || x > offset_x + render_width || y < offset_y ||
|
|
y > offset_y + render_height) {
|
|
return;
|
|
}
|
|
|
|
RemoteAction action{};
|
|
action.type = ControlType::mouse;
|
|
action.m.x = std::clamp((x - offset_x) / render_width, 0.0f, 1.0f);
|
|
action.m.y = std::clamp((y - offset_y) / render_height, 0.0f, 1.0f);
|
|
// Slint enum order: Cancel=0, Down=1, Up=2, Move=3 and
|
|
// Other=0, Left=1, Right=2, Middle=3.
|
|
if (kind == 3) {
|
|
action.m.flag = MouseFlag::move;
|
|
} else if (kind == 1) {
|
|
if (button == 1) action.m.flag = MouseFlag::left_down;
|
|
else if (button == 2) action.m.flag = MouseFlag::right_down;
|
|
else if (button == 3) action.m.flag = MouseFlag::middle_down;
|
|
else return;
|
|
} else if (kind == 2) {
|
|
if (button == 1) action.m.flag = MouseFlag::left_up;
|
|
else if (button == 2) action.m.flag = MouseFlag::right_up;
|
|
else if (button == 3) action.m.flag = MouseFlag::middle_up;
|
|
else return;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
controlled_remote_id_ = props->remote_id_;
|
|
const std::string message = action.to_json();
|
|
SendDataFrame(props->peer_, message.c_str(), message.size(),
|
|
props->mouse_label_.c_str());
|
|
}
|
|
|
|
void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
|
|
float y) {
|
|
auto props = SelectedSession();
|
|
if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
|
|
return;
|
|
}
|
|
const auto size = (*ui_->stream)->window().size();
|
|
const float scale = (*ui_->stream)->window().scale_factor();
|
|
const float width = size.width / scale;
|
|
const float height =
|
|
size.height / scale -
|
|
(fullscreen_button_pressed_
|
|
? 0.0f
|
|
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
RemoteAction action{};
|
|
action.type = ControlType::mouse;
|
|
action.m.x = std::clamp(x / width, 0.0f, 1.0f);
|
|
action.m.y = std::clamp(y / height, 0.0f, 1.0f);
|
|
if (std::abs(delta_y) >= std::abs(delta_x)) {
|
|
action.m.flag = MouseFlag::wheel_vertical;
|
|
action.m.s = delta_y > 0 ? 1 : delta_y < 0 ? -1 : 0;
|
|
} else {
|
|
action.m.flag = MouseFlag::wheel_horizontal;
|
|
action.m.s = delta_x > 0 ? 1 : delta_x < 0 ? -1 : 0;
|
|
}
|
|
if (action.m.s == 0) {
|
|
return;
|
|
}
|
|
controlled_remote_id_ = props->remote_id_;
|
|
const std::string message = action.to_json();
|
|
SendDataFrame(props->peer_, message.c_str(), message.size(),
|
|
props->mouse_label_.c_str());
|
|
}
|
|
|
|
void GuiApplication::SendKeyInput(const std::string &text, bool pressed,
|
|
bool control, bool alt, bool shift,
|
|
bool meta) {
|
|
(void)control;
|
|
(void)alt;
|
|
(void)shift;
|
|
(void)meta;
|
|
if (auto props = SelectedSession()) {
|
|
controlled_remote_id_ = props->remote_id_;
|
|
focused_remote_id_ = props->remote_id_;
|
|
}
|
|
const int virtual_key = SlintKeyToWindowsVk(text);
|
|
if (virtual_key >= 0) {
|
|
keyboard_.SendKeyCommand(virtual_key, pressed);
|
|
}
|
|
}
|
|
|
|
std::string GuiApplication::OpenFileDialog(const std::string &title) {
|
|
#if defined(__APPLE__)
|
|
return OpenNativeFileDialog(title);
|
|
#else
|
|
const char *path =
|
|
tinyfd_openFileDialog(title.c_str(), "", 0, nullptr, nullptr, 0);
|
|
return path ? path : "";
|
|
#endif
|
|
}
|
|
|
|
bool GuiApplication::OpenUrl(const std::string &url) {
|
|
return SDL_OpenURL(url.c_str());
|
|
}
|
|
|
|
void GuiApplication::Cleanup() {
|
|
if (!ui_) {
|
|
SDL_Quit();
|
|
return;
|
|
}
|
|
ui_->timer.stop();
|
|
#if _WIN32 && CROSSDESK_PORTABLE
|
|
JoinPortableWindowsServiceInstallThread();
|
|
#endif
|
|
clipboard_.Shutdown();
|
|
keyboard_.ForceReleasePressedKeys();
|
|
devices_.DestroyDevices();
|
|
devices_.DestroyFactories();
|
|
CloseAllRemoteSessions();
|
|
WaitForThumbnailSaveTasks();
|
|
devices_.DestroyAudioOutput();
|
|
if (ui_->stream) {
|
|
(*ui_->stream)->hide();
|
|
}
|
|
if (ui_->server) {
|
|
(*ui_->server)->hide();
|
|
}
|
|
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
|
if (ui_->tray) {
|
|
ui_->tray->RemoveTrayIcon();
|
|
ui_->tray.reset();
|
|
}
|
|
#endif
|
|
ui_.reset();
|
|
SDL_Quit();
|
|
}
|
|
|
|
} // namespace crossdesk
|