Compare commits

..
12 changed files with 523 additions and 427 deletions
+6 -2
View File
@@ -1,6 +1,7 @@
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
#define CROSSDESK_GUI_APPLICATION_STATE_H_
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
@@ -119,8 +120,11 @@ struct StreamWindowState {
};
struct ServerWindowState {
bool need_to_create_server_window_ = false;
bool need_to_destroy_server_window_ = false;
// Transport callbacks run outside the UI thread. Keep the two lifecycle
// requests atomic so a disconnect cannot be lost while the Slint timer is
// deciding whether to create or destroy the controlled-side window.
std::atomic<bool> need_to_create_server_window_{false};
std::atomic<bool> need_to_destroy_server_window_{false};
bool server_window_created_ = false;
bool server_window_inited_ = false;
int server_window_width_default_ = 250;
+49 -39
View File
@@ -101,8 +101,7 @@ private:
&bytes_after, &data) == Success &&
actual_type == XA_WINDOW && actual_format == 32 && item_count == 1 &&
data) {
const ::Window active_window =
*reinterpret_cast<const ::Window*>(data);
const ::Window active_window = *reinterpret_cast<const ::Window*>(data);
XFree(data);
return active_window;
}
@@ -165,9 +164,8 @@ bool X11WindowMatches(Display* display, ::Window x11_window,
int root_x = 0;
int root_y = 0;
::Window child = 0;
if (!XTranslateCoordinates(display, x11_window,
DefaultRootWindow(display), 0, 0, &root_x,
&root_y, &child)) {
if (!XTranslateCoordinates(display, x11_window, DefaultRootWindow(display), 0,
0, &root_x, &root_y, &child)) {
return false;
}
@@ -269,17 +267,15 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
size_t line_start = 0;
while (line_start < markdown.size()) {
const size_t newline = markdown.find('\n', line_start);
const size_t line_end = newline == std::string_view::npos
? markdown.size()
: newline;
const size_t line_end =
newline == std::string_view::npos ? markdown.size() : newline;
std::string_view line = markdown.substr(line_start, line_end - line_start);
if (!line.empty() && line.back() == '\r') {
line.remove_suffix(1);
}
if (std::all_of(line.begin(), line.end(), [](unsigned char ch) {
return std::isspace(ch) != 0;
})) {
if (std::all_of(line.begin(), line.end(),
[](unsigned char ch) { return std::isspace(ch) != 0; })) {
section_gap = !blocks.empty();
if (newline == std::string_view::npos) {
break;
@@ -306,11 +302,11 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
const std::string block_markdown(line);
ParsedReleaseNoteBlock block;
if (const auto parsed =
slint::StyledText::from_markdown(block_markdown)) {
if (const auto parsed = slint::StyledText::from_markdown(block_markdown)) {
block.content = *parsed;
} else {
LOG_WARN("Failed to parse a release-note Markdown block; using plain text");
LOG_WARN(
"Failed to parse a release-note Markdown block; using plain text");
block.content = slint::StyledText::from_plain_text(block_markdown);
}
block.section_gap = section_gap;
@@ -958,12 +954,13 @@ bool GuiApplication::InitializeSDL() {
const bool video_driver_allows_x11 =
requested_video_driver == nullptr || requested_video_driver[0] == '\0' ||
std::strcmp(requested_video_driver, "x11") == 0;
use_xwayland_gui_ =
IsWaylandSession() && HasNonEmptyEnvironmentVariable("DISPLAY") &&
use_xwayland_gui_ = IsWaylandSession() &&
HasNonEmptyEnvironmentVariable("DISPLAY") &&
video_driver_allows_x11;
if (use_xwayland_gui_) {
setenv("SDL_VIDEODRIVER", "x11", 1);
LOG_INFO("Wayland session detected; using XWayland for positioned GUI "
LOG_INFO(
"Wayland session detected; using XWayland for positioned GUI "
"windows while retaining Wayland portal capture and input");
}
#endif
@@ -1011,8 +1008,7 @@ void GuiApplication::InitializeUi() {
}
#endif
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
for (const auto& parsed :
ParseReleaseNotesMarkdownForSlint(release_notes_)) {
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
ui::ReleaseNoteBlock block;
block.content = parsed.content;
block.section_gap = parsed.section_gap;
@@ -1159,10 +1155,10 @@ void GuiApplication::InitializeUi() {
#endif
BindStreamCallbacks();
(*ui_->stream)->show();
PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
PositionWindowAtCenter(
(*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalWidth,
StreamWindowLogicalHeight(
(*ui_->stream)->get_custom_titlebar()));
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()));
ui_->main->hide();
} else if (ui_->capture_mode && ui_->capture_page == "server") {
ui_->server.emplace(ui::ServerWindow::create());
@@ -2078,6 +2074,11 @@ void GuiApplication::SyncMainWindow() {
}
model.push_back(std::move(item));
}
// LoadThumbnail already orders connections by last update time. Keep that
// order within each presence group while placing online devices first.
std::stable_partition(
model.begin(), model.end(),
[](const ui::RecentConnection& connection) { return connection.online; });
ui_->recent_model->set_vector(std::move(model));
}
@@ -2230,8 +2231,7 @@ void GuiApplication::SyncXWaylandWindowActivation() {
return;
}
if (const auto process_id =
X11GetWindowProcessId(display, active_window);
if (const auto process_id = X11GetWindowProcessId(display, active_window);
process_id && *process_id != static_cast<unsigned long>(getpid())) {
ui_->main->set_window_active(false);
if (ui_->stream) {
@@ -2244,8 +2244,8 @@ void GuiApplication::SyncXWaylandWindowActivation() {
X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) {
(*ui_->stream)
->set_window_active(X11WindowMatches(
display, active_window, (*ui_->stream)->window()));
->set_window_active(
X11WindowMatches(display, active_window, (*ui_->stream)->window()));
}
}
#endif
@@ -2284,8 +2284,7 @@ void GuiApplication::SyncStreamWindow() {
if (!ui_->stream) {
return;
}
(*ui_->stream)
->set_window_maximized((*ui_->stream)->window().is_maximized());
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
#if defined(__APPLE__)
if (ui_->stream_live_resize_configuration_attempts > 0) {
@@ -2297,10 +2296,10 @@ void GuiApplication::SyncStreamWindow() {
}
#endif
if (ui_->stream_initial_position_attempts > 0) {
if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
if (PositionWindowAtCenter(
(*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalWidth,
StreamWindowLogicalHeight(
(*ui_->stream)->get_custom_titlebar()))) {
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()))) {
--ui_->stream_initial_position_attempts;
} else {
ui_->stream_initial_position_attempts = 0;
@@ -2534,7 +2533,22 @@ void GuiApplication::SyncStreamWindow() {
}
void GuiApplication::SyncServerWindow() {
if (need_to_create_server_window_ && !ui_->server) {
// The connection map is authoritative. Lifecycle flags record callback
// intent, but callbacks for different controllers can cross each other, so
// the final decision must reflect the current connected-controller set.
need_to_create_server_window_.exchange(false, std::memory_order_acq_rel);
need_to_destroy_server_window_.exchange(false, std::memory_order_acq_rel);
bool has_connected_controller = false;
{
std::shared_lock lock(connection_status_mutex_);
has_connected_controller =
std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
}
if (has_connected_controller && !ui_->server) {
ui_->server.emplace(ui::ServerWindow::create());
RegisterFontAwesome((*ui_->server)->window());
(*ui_->server)->set_controllers(ui_->controller_model);
@@ -2554,15 +2568,13 @@ void GuiApplication::SyncServerWindow() {
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) {
if (!has_connected_controller && 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;
@@ -2863,8 +2875,7 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
: 0.0f;
const float available_height =
size.height / scale - titlebar_height -
(fullscreen_button_pressed_
? 0.0f
(fullscreen_button_pressed_ ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
float render_width = available_width;
float render_height = available_height;
@@ -2939,8 +2950,7 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
: 0.0f;
const float height =
size.height / scale - titlebar_height -
(fullscreen_button_pressed_
? 0.0f
(fullscreen_button_pressed_ ? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
if (width <= 0 || height <= 0) {
return;
+2 -4
View File
@@ -220,10 +220,8 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
return -1;
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
return 0;
}
// SDL represents key auto-repeat as additional key-down events. Forward
// them so the fallback path has the same long-press behavior as native hooks.
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
if (key_code < 0) {
return 0;
+62 -9
View File
@@ -1,5 +1,3 @@
#include "runtime/gui_runtime.h"
#include <algorithm>
#include <chrono>
#include <cstring>
@@ -11,7 +9,9 @@
#include <vector>
#include "localization.h"
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
namespace crossdesk {
@@ -161,6 +161,63 @@ void GuiRuntime::HandleConnectionTimeouts() {
}
}
void GuiRuntime::HandleServerControllerDisconnected(
const std::string& remote_id, const char* reason) {
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
bool has_connected_controller = false;
bool has_web_controller = false;
std::string remaining_controller_id;
{
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
connection_host_names_.erase(remote_id);
for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) {
continue;
}
has_connected_controller = true;
has_web_controller =
has_web_controller || id.find("web") != std::string::npos;
if (remaining_controller_id.empty()) {
remaining_controller_id = id;
}
}
}
show_cursor_ = has_web_controller;
if (has_connected_controller) {
remote_client_id_ = remaining_controller_id;
return;
}
need_to_create_server_window_.store(false, std::memory_order_release);
need_to_destroy_server_window_.store(true, std::memory_order_release);
is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on subsequent
// reconnects.
start_screen_capturer_ = true;
LOG_INFO(
"Keeping Wayland screen capturer running after disconnect to "
"preserve reconnect stability");
} else {
start_screen_capturer_ = false;
}
#else
start_screen_capturer_ = false;
#endif
start_speaker_capturer_ = false;
start_mouse_controller_ = false;
start_keyboard_capturer_ = false;
remote_client_id_.clear();
if (audio_capture_) {
devices_.StopSpeakerCapturer();
audio_capture_ = false;
}
devices_.ResetToInitialDisplay();
}
int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
const char* password,
bool remember_password) {
@@ -300,8 +357,7 @@ void GuiRuntime::ResetRemoteSessionResources(
}
}
std::shared_ptr<GuiRuntime::RemoteSession>
GuiRuntime::FindRemoteSession(
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
const std::string& remote_id) {
if (remote_id.empty()) {
return nullptr;
@@ -315,7 +371,6 @@ GuiRuntime::FindRemoteSession(
return it->second;
}
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
bool remember_password, bool bypass_presence_check) {
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
@@ -336,8 +391,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
focused_remote_id_ = remote_id;
// std::shared_lock shared_lock(remote_sessions_mutex_);
bool exists =
(remote_sessions_.find(remote_id) != remote_sessions_.end());
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
// shared_lock.unlock();
if (!exists) {
@@ -347,8 +401,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
{
// std::unique_lock unique_lock(remote_sessions_mutex_);
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
remote_sessions_[remote_id] =
std::make_shared<RemoteSession>();
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
auto props = remote_sessions_[remote_id];
props->local_id_ = "C-" + std::string(client_id_);
props->remote_id_ = remote_id;
+7 -7
View File
@@ -8,9 +8,9 @@
#include "features/devices/session_device_manager.h"
#include "features/file_transfer/file_transfer_manager.h"
#include "features/input/keyboard_controller.h"
#include "features/settings/settings_manager.h"
#include "runtime/gui_state.h"
#include "runtime/peer_event_handler.h"
#include "features/settings/settings_manager.h"
namespace crossdesk {
@@ -46,21 +46,21 @@ protected:
void HandleConnectionStatusChange();
void HandlePendingPresenceProbe();
void HandleConnectionTimeouts();
void HandleServerControllerDisconnected(const std::string& remote_id,
const char* reason);
void HandleWindowsServiceIntegration();
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
void CloseAllRemoteSessions();
void ResetRemoteSessionResources(
std::shared_ptr<RemoteSession> props);
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
void WaitForThumbnailSaveTasks();
std::shared_ptr<RemoteSession>
FindRemoteSession(const std::string &remote_id);
std::shared_ptr<RemoteSession> FindRemoteSession(
const std::string& remote_id);
void ResetRemoteServiceStatus(RemoteSession& props);
void ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus& status);
RemoteUnlockState
GetRemoteUnlockState(const RemoteSession &props) const;
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
#endif
+24 -71
View File
@@ -88,7 +88,6 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
}
}
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
size_t user_id_size, void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
@@ -155,8 +154,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
void* user_data) {
auto* handler = static_cast<PeerEventHandler*>(user_data);
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime)
return;
if (!runtime) return;
std::string remote_id(user_id, user_id_size);
std::shared_ptr<GuiRuntime::RemoteSession> props;
@@ -202,11 +200,13 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
@@ -215,7 +215,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
@@ -315,11 +316,13 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
strncpy(remote_action.i.display_list[i],
runtime->devices_.display_info_list()[i].name.c_str(),
runtime->devices_.display_info_list()[i].name.length());
remote_action.i.display_list
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
remote_action.i
.display_list[i][runtime->devices_.display_info_list()[i]
.name.length()] = '\0';
remote_action.i.left[i] =
runtime->devices_.display_info_list()[i].left;
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
remote_action.i.top[i] =
runtime->devices_.display_info_list()[i].top;
remote_action.i.right[i] =
runtime->devices_.display_info_list()[i].right;
remote_action.i.bottom[i] =
@@ -328,17 +331,22 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
std::string host_name = GetHostName();
remote_action.type = ControlType::host_infomation;
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
memcpy(&remote_action.i.host_name, host_name.data(),
host_name.size());
remote_action.i.host_name[host_name.size()] = '\0';
remote_action.i.host_name_size = host_name.size();
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
int ret =
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
remote_action_codec::Free(remote_action);
}
runtime->need_to_create_server_window_ = true;
runtime->need_to_destroy_server_window_.store(
false, std::memory_order_release);
runtime->need_to_create_server_window_.store(true,
std::memory_order_release);
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
runtime->start_speaker_capturer_ = true;
@@ -347,7 +355,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
runtime->connection_status_.end(),
[](const auto& kv) {
return kv.first.find("web") != std::string::npos;
})) {
runtime->show_cursor_ = true;
@@ -359,64 +368,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed:
case ConnectionStatus::Closed: {
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
runtime->HandleServerControllerDisconnected(remote_id,
"connection_closed");
bool all_disconnected = false;
{
std::shared_lock lock(runtime->connection_status_mutex_);
all_disconnected =
std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
});
}
if (all_disconnected) {
runtime->need_to_destroy_server_window_ = true;
runtime->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on
// subsequent reconnects.
runtime->start_screen_capturer_ = true;
LOG_INFO("Keeping Wayland screen capturer running after "
"disconnect to preserve reconnect stability");
} else {
runtime->start_screen_capturer_ = false;
}
#else
runtime->start_screen_capturer_ = false;
#endif
runtime->start_speaker_capturer_ = false;
runtime->start_mouse_controller_ = false;
runtime->start_keyboard_capturer_ = false;
runtime->remote_client_id_ = "";
if (props)
props->connection_established_ = false;
if (runtime->audio_capture_) {
runtime->devices_.StopSpeakerCapturer();
runtime->audio_capture_ = false;
}
{
std::unique_lock lock(runtime->connection_status_mutex_);
runtime->connection_status_.erase(remote_id);
runtime->connection_host_names_.erase(remote_id);
}
runtime->devices_.ResetToInitialDisplay();
}
{
std::shared_lock lock(runtime->connection_status_mutex_);
if (std::all_of(runtime->connection_status_.begin(),
runtime->connection_status_.end(), [](const auto &kv) {
return kv.first.find("web") == std::string::npos;
})) {
runtime->show_cursor_ = false;
}
}
break;
}
default:
+87 -51
View File
@@ -282,6 +282,7 @@ export component MainWindow inherits Window {
private property <bool> menu-open: false;
in-out property <bool> settings-open: false;
private property <length> settings-scroll-y: 0px;
in-out property <bool> self-host-settings-open: false;
in-out property <bool> about-open: false;
in-out property <bool> update-open: root.update-available;
@@ -318,6 +319,13 @@ export component MainWindow inherits Window {
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
changed settings-open => {
if root.settings-open {
root.settings-scroll-y = 0px;
focus-sink.focus();
}
}
reset-remote-id => {
root.remote-id-input = "";
remote-id-editor.focus();
@@ -1388,9 +1396,9 @@ export component MainWindow inherits Window {
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: root.compact-language ? 219px : 190px;
y: 9px;
y: 60px;
width: root.compact-language ? 202px : 269px;
height: 432px;
height: 330px;
border-radius: 7px;
Rectangle {
@@ -1405,55 +1413,86 @@ export component MainWindow inherits Window {
Text { x: 6px; text: UiStrings.settings; font-size: ImGuiFontStyle.base; font-weight: 600; color: #202124; vertical-alignment: center; }
}
Text { x: 8px; y: 31px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
settings-scroll := ScrollView {
x: 0px;
y: 21px;
width: parent.width;
height: 265px;
viewport-y <=> root.settings-scroll-y;
Text { x: 8px; y: 61px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 60px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
settings-content := VerticalLayout {
// Reserve space for the overlaid vertical scrollbar.
width: settings-scroll.width;
padding-top: 8px;
padding-bottom: 8px;
spacing: 0px;
Text { x: 8px; y: 91px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 90px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.quality-low, UiStrings.quality-medium, UiStrings.quality-high]; current-index <=> root.video-quality-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.frame-rate; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: ["30 fps", "60 fps"]; current-index <=> root.frame-rate-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
}
Rectangle {
height: 30px;
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 154px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 106px : 166px; y: 3px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
}
}
}
Text { x: 8px; y: 121px; width: root.compact-language ? 108px : 176px; height: 24px; text: UiStrings.codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 120px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; color: root.hardware-codec-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: root.hardware-codec-available && !root.settings-session-active; }
Rectangle { x: 7px; y: 177px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 181px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.turn-relay; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 181px; checked <=> root.turn-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 207px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 211px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 211px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 237px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: 8px; y: 241px; width: root.compact-language ? 70px : 220px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 241px; checked <=> root.self-hosted-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 267px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 271px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.autostart; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 271px; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 297px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 301px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.daemon; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 301px; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 327px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 331px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.minimize-to-tray; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 331px; checked <=> root.minimize-to-tray-enabled; enabled: !root.settings-session-active; }
Rectangle { x: 7px; y: 357px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 361px; width: root.compact-language ? 108px : 168px; height: 24px; text: UiStrings.file-save-path; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
CompactButton { x: root.compact-language ? 120px : 180px; y: 361px; width: root.compact-language ? 73px : 80px; text: root.file-save-path == "" ? UiStrings.default-desktop : root.file-save-path; enabled: !root.settings-session-active; clicked => { root.browse-save-path(); } }
CompactButton { x: root.compact-language ? 63px : 89px; y: 396px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 396px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: root.compact-language ? 63px : 89px; y: 297px; primary: true; text: UiStrings.ok; clicked => { root.save-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 104px : 130px; y: 297px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-settings(); root.settings-open = false; root.self-host-settings-open = false; } }
}
}
@@ -1492,7 +1531,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-host; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 61px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.server-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
@@ -1506,7 +1544,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.server-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
Text { x: 8px; y: 91px; width: root.compact-language ? 88px : 165px; height: 24px; text: UiStrings.coturn-port; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
Rectangle {
x: root.compact-language ? 100px : 173px;
@@ -1520,7 +1557,6 @@ export component MainWindow inherits Window {
LineEdit { x: 1px; y: 1.5px; width: parent.width - 2px; height: parent.height - 2px; text <=> root.coturn-port; input-type: InputType.number; font-size: ImGuiFontStyle.body; }
LineEditFocusUnderlineMask { x: 5px; y: parent.height - 3px; width: parent.width - 10px; }
}
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
CompactButton { x: root.compact-language ? 93px : 135px; y: 130px; primary: true; text: UiStrings.ok; clicked => { root.save-self-hosted-settings(); root.self-host-settings-open = false; } }
CompactButton { x: root.compact-language ? 134px : 176px; y: 130px; width: root.compact-language ? 34px : 56px; text: UiStrings.cancel; clicked => { root.cancel-self-hosted-settings(); root.self-host-settings-open = false; } }
}
+4 -1
View File
@@ -508,7 +508,10 @@ export component StreamWindow inherits Window {
&& (event.kind == PointerEventKind.up
|| event.kind == PointerEventKind.cancel) {
root.end-control-drag();
} else if !root.control-dragging {
// The control bar overlays the remote video. Events in
// its full rectangle (including gaps between buttons)
// are local UI input and must never reach the peer.
} else if !root.control-dragging && !over-control {
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
}
}
+17
View File
@@ -73,6 +73,10 @@ int main() {
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
const std::string connection_runtime_cpp =
ReadFile(repo_root / "src/gui/runtime/connection_runtime.cpp");
const std::string peer_event_handler_cpp =
ReadFile(repo_root / "src/gui/runtime/peer_event_handler.cpp");
const std::string application_state_h =
ReadFile(repo_root / "src/gui/application/application_state.h");
bool ok = true;
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
@@ -81,6 +85,11 @@ int main() {
"\"Remote unavailable\"");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"ConnectionStatus::RemoteUnavailable");
ok &= ExpectContains("peer_connection.cpp", peer_connection,
"IsCurrentPeerConnection");
ok &= ExpectContains(
"peer_connection.cpp", peer_connection,
"on_connection_status_(ConnectionStatus::Closed, remote_user_id.data()");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
"\"Device offline\"");
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
@@ -105,6 +114,14 @@ int main() {
"kConnectionAttemptTimeout");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"connection_attempt_started_at_");
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
"HandleServerControllerDisconnected");
ok &= ExpectContains("peer_event_handler.cpp", peer_event_handler_cpp,
"HandleServerControllerDisconnected(remote_id");
ok &= ExpectNotContains("connection_runtime.cpp", connection_runtime_cpp,
"ControllerHeartbeat");
ok &= ExpectContains("application_state.h", application_state_h,
"std::atomic<bool> need_to_destroy_server_window_");
return ok ? 0 : 1;
}
+26 -18
View File
@@ -84,14 +84,16 @@ int main() {
"root.shortcut-menu-open = false;");
ok &= ExpectContains("stream_window.slint", stream,
"if root.display-menu-open: Rectangle");
ok &= ExpectContains("stream_window.slint", stream,
"display-touch.has-hover");
ok &=
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"root.switch-display(index)");
ok &= ExpectContains("stream_window.slint", stream,
"if root.shortcut-menu-open: Rectangle");
ok &=
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
ok &= ExpectContains("stream_window.slint", stream,
"shortcut-touch.has-hover");
"} else if !root.control-dragging && !over-control {");
ok &= ExpectContains("common.slint", common,
"if touch.has-hover && root.tooltip != \"\": Rectangle");
@@ -100,32 +102,38 @@ int main() {
"tooltip: StreamStrings.select-display");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.send-shortcut");
ok &= ExpectContains("stream_window.slint", stream,
"root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse");
ok &= ExpectContains("stream_window.slint", stream,
ok &=
ExpectContains("stream_window.slint", stream,
"root.mouse-control-enabled ? StreamStrings.release-mouse "
": StreamStrings.control-mouse");
ok &= ExpectContains(
"stream_window.slint", stream,
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.select-file");
ok &= ExpectContains("stream_window.slint", stream,
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats");
ok &= ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen");
"root.stats-visible ? StreamStrings.hide-stats : "
"StreamStrings.show-stats");
ok &=
ExpectContains("stream_window.slint", stream,
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
": StreamStrings.fullscreen");
ok &= ExpectContains("stream_window.slint", stream,
"tooltip: StreamStrings.disconnect");
ok &= ExpectContains("stream_window.slint", stream,
"root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control");
"root.control-expanded ? StreamStrings.collapse-control "
": StreamStrings.expand-control");
ok &= ExpectContains("gui_application.cpp", application,
"set_select_display(");
ok &= ExpectContains("gui_application.cpp", application,
"set_show_stats(");
ok &= ExpectContains("gui_application.cpp", application,
"set_expand_control(");
ok &=
ExpectContains("gui_application.cpp", application, "set_select_display(");
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
ok &=
ExpectContains("gui_application.cpp", application, "set_expand_control(");
ok &= ExpectContains("gui_application.cpp", application,
"set_collapse_control(");
ok &= ExpectContains("gui_application.cpp", application,
"(*ui_->stream)->global<ui::StreamStrings>()");
ok &= ExpectNotContains("gui_application.cpp", application,
"#include <imgui");
ok &=
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
return ok ? 0 : 1;
}
+14
View File
@@ -90,6 +90,20 @@ int main() {
window->set_about_open(true);
assert(window->get_settings_open());
assert(window->get_about_open());
if (const char *snapshot_path =
std::getenv("CROSSDESK_SETTINGS_UI_SNAPSHOT")) {
window->set_about_open(false);
window->set_connection_dialog_open(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 6;
}
window->set_connection_dialog_open(true);
window->set_about_open(true);
}
window->set_remote_id_input("987654321");
window->invoke_reset_remote_id();
assert(std::string(window->get_remote_id_input()).empty());