mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-20 01:55:48 +08:00
Compare commits
13
Commits
2c20599800
..
v1.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3d15f8f9a | ||
|
|
a6e8038286 | ||
|
|
765452e3c9 | ||
|
|
7cd0d6a351 | ||
|
|
c01b52eac5 | ||
|
|
737cf9b558 | ||
|
|
6f5faa7111 | ||
|
|
3a72e4849e | ||
|
|
43e44af730 | ||
|
|
97e592861c | ||
|
|
31b8983328 | ||
|
|
522f519d38 | ||
|
|
37c4fe2e8a |
@@ -5,6 +5,9 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/keysym.h>
|
||||||
|
|
||||||
#include <dbus/dbus.h>
|
#include <dbus/dbus.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -119,8 +120,11 @@ struct StreamWindowState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct ServerWindowState {
|
struct ServerWindowState {
|
||||||
bool need_to_create_server_window_ = false;
|
// Transport callbacks run outside the UI thread. Keep the two lifecycle
|
||||||
bool need_to_destroy_server_window_ = false;
|
// 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_created_ = false;
|
||||||
bool server_window_inited_ = false;
|
bool server_window_inited_ = false;
|
||||||
int server_window_width_default_ = 250;
|
int server_window_width_default_ = 250;
|
||||||
|
|||||||
@@ -28,12 +28,17 @@
|
|||||||
#include "crossdesk_ui.h"
|
#include "crossdesk_ui.h"
|
||||||
#include "fa_solid_900.h"
|
#include "fa_solid_900.h"
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
|
#include "platform.h"
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
#include "platform/tray/win_tray.h"
|
#include "platform/tray/win_tray.h"
|
||||||
#elif defined(__APPLE__)
|
#elif defined(__APPLE__)
|
||||||
#include "platform/tray/mac_tray.h"
|
#include "platform/tray/mac_tray.h"
|
||||||
#include "platform/window_drag.h"
|
#include "platform/window_drag.h"
|
||||||
#elif defined(__linux__)
|
#elif defined(__linux__)
|
||||||
|
#include <X11/Xatom.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "platform/tray/linux_tray.h"
|
#include "platform/tray/linux_tray.h"
|
||||||
#endif
|
#endif
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
@@ -44,6 +49,138 @@ namespace {
|
|||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
bool HasNonEmptyEnvironmentVariable(const char* name) {
|
||||||
|
const char* value = std::getenv(name);
|
||||||
|
return value != nullptr && value[0] != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedUnsetEnvironmentVariable {
|
||||||
|
public:
|
||||||
|
explicit ScopedUnsetEnvironmentVariable(const char* name) : name_(name) {
|
||||||
|
if (const char* value = std::getenv(name_)) {
|
||||||
|
original_value_ = value;
|
||||||
|
}
|
||||||
|
unsetenv(name_);
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedUnsetEnvironmentVariable() {
|
||||||
|
if (original_value_) {
|
||||||
|
setenv(name_, original_value_->c_str(), 1);
|
||||||
|
} else {
|
||||||
|
unsetenv(name_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedUnsetEnvironmentVariable(const ScopedUnsetEnvironmentVariable&) =
|
||||||
|
delete;
|
||||||
|
ScopedUnsetEnvironmentVariable& operator=(
|
||||||
|
const ScopedUnsetEnvironmentVariable&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const char* name_;
|
||||||
|
std::optional<std::string> original_value_;
|
||||||
|
};
|
||||||
|
|
||||||
|
::Window X11GetActiveWindow(Display* display) {
|
||||||
|
if (!display) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ::Window root = DefaultRootWindow(display);
|
||||||
|
const Atom active_window_atom =
|
||||||
|
XInternAtom(display, "_NET_ACTIVE_WINDOW", True);
|
||||||
|
if (active_window_atom != None) {
|
||||||
|
Atom actual_type = None;
|
||||||
|
int actual_format = 0;
|
||||||
|
unsigned long item_count = 0;
|
||||||
|
unsigned long bytes_after = 0;
|
||||||
|
unsigned char* data = nullptr;
|
||||||
|
if (XGetWindowProperty(display, root, active_window_atom, 0, 1, False,
|
||||||
|
XA_WINDOW, &actual_type, &actual_format, &item_count,
|
||||||
|
&bytes_after, &data) == Success &&
|
||||||
|
actual_type == XA_WINDOW && actual_format == 32 && item_count == 1 &&
|
||||||
|
data) {
|
||||||
|
const ::Window active_window = *reinterpret_cast<const ::Window*>(data);
|
||||||
|
XFree(data);
|
||||||
|
return active_window;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
XFree(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::Window focused_window = 0;
|
||||||
|
int revert_to = RevertToNone;
|
||||||
|
XGetInputFocus(display, &focused_window, &revert_to);
|
||||||
|
return focused_window == None || focused_window == PointerRoot
|
||||||
|
? 0
|
||||||
|
: focused_window;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<unsigned long> X11GetWindowProcessId(Display* display,
|
||||||
|
::Window x11_window) {
|
||||||
|
if (!display || !x11_window) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Atom process_id_atom = XInternAtom(display, "_NET_WM_PID", True);
|
||||||
|
if (process_id_atom == None) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
Atom actual_type = None;
|
||||||
|
int actual_format = 0;
|
||||||
|
unsigned long item_count = 0;
|
||||||
|
unsigned long bytes_after = 0;
|
||||||
|
unsigned char* data = nullptr;
|
||||||
|
const int status = XGetWindowProperty(
|
||||||
|
display, x11_window, process_id_atom, 0, 1, False, XA_CARDINAL,
|
||||||
|
&actual_type, &actual_format, &item_count, &bytes_after, &data);
|
||||||
|
if (status != Success || actual_type != XA_CARDINAL || actual_format != 32 ||
|
||||||
|
item_count != 1 || !data) {
|
||||||
|
if (data) {
|
||||||
|
XFree(data);
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto process_id = *reinterpret_cast<const unsigned long*>(data);
|
||||||
|
XFree(data);
|
||||||
|
return process_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool X11WindowMatches(Display* display, ::Window x11_window,
|
||||||
|
const slint::Window& slint_window) {
|
||||||
|
if (!display || !x11_window) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XWindowAttributes attributes;
|
||||||
|
if (!XGetWindowAttributes(display, x11_window, &attributes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto position = slint_window.position();
|
||||||
|
const auto size = slint_window.size();
|
||||||
|
constexpr int kGeometryTolerance = 4;
|
||||||
|
return std::abs(root_x - position.x) <= kGeometryTolerance &&
|
||||||
|
std::abs(root_y - position.y) <= kGeometryTolerance &&
|
||||||
|
std::abs(attributes.width - static_cast<int>(size.width)) <=
|
||||||
|
kGeometryTolerance &&
|
||||||
|
std::abs(attributes.height - static_cast<int>(size.height)) <=
|
||||||
|
kGeometryTolerance;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
HICON LoadSlintTrayIcon() {
|
HICON LoadSlintTrayIcon() {
|
||||||
HMODULE module = GetModuleHandleW(nullptr);
|
HMODULE module = GetModuleHandleW(nullptr);
|
||||||
@@ -130,17 +267,15 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
|
|||||||
size_t line_start = 0;
|
size_t line_start = 0;
|
||||||
while (line_start < markdown.size()) {
|
while (line_start < markdown.size()) {
|
||||||
const size_t newline = markdown.find('\n', line_start);
|
const size_t newline = markdown.find('\n', line_start);
|
||||||
const size_t line_end = newline == std::string_view::npos
|
const size_t line_end =
|
||||||
? markdown.size()
|
newline == std::string_view::npos ? markdown.size() : newline;
|
||||||
: newline;
|
|
||||||
std::string_view line = markdown.substr(line_start, line_end - line_start);
|
std::string_view line = markdown.substr(line_start, line_end - line_start);
|
||||||
if (!line.empty() && line.back() == '\r') {
|
if (!line.empty() && line.back() == '\r') {
|
||||||
line.remove_suffix(1);
|
line.remove_suffix(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (std::all_of(line.begin(), line.end(), [](unsigned char ch) {
|
if (std::all_of(line.begin(), line.end(),
|
||||||
return std::isspace(ch) != 0;
|
[](unsigned char ch) { return std::isspace(ch) != 0; })) {
|
||||||
})) {
|
|
||||||
section_gap = !blocks.empty();
|
section_gap = !blocks.empty();
|
||||||
if (newline == std::string_view::npos) {
|
if (newline == std::string_view::npos) {
|
||||||
break;
|
break;
|
||||||
@@ -167,11 +302,11 @@ std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
|
|||||||
|
|
||||||
const std::string block_markdown(line);
|
const std::string block_markdown(line);
|
||||||
ParsedReleaseNoteBlock block;
|
ParsedReleaseNoteBlock block;
|
||||||
if (const auto parsed =
|
if (const auto parsed = slint::StyledText::from_markdown(block_markdown)) {
|
||||||
slint::StyledText::from_markdown(block_markdown)) {
|
|
||||||
block.content = *parsed;
|
block.content = *parsed;
|
||||||
} else {
|
} 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.content = slint::StyledText::from_plain_text(block_markdown);
|
||||||
}
|
}
|
||||||
block.section_gap = section_gap;
|
block.section_gap = section_gap;
|
||||||
@@ -496,6 +631,12 @@ float ServerWindowLogicalHeight(bool collapsed) {
|
|||||||
|
|
||||||
constexpr float kStreamWindowLogicalWidth = 1280.0f;
|
constexpr float kStreamWindowLogicalWidth = 1280.0f;
|
||||||
constexpr float kStreamWindowLogicalHeight = 720.0f;
|
constexpr float kStreamWindowLogicalHeight = 720.0f;
|
||||||
|
constexpr float kWaylandTitlebarLogicalHeight = 32.0f;
|
||||||
|
|
||||||
|
float StreamWindowLogicalHeight(bool custom_titlebar) {
|
||||||
|
return kStreamWindowLogicalHeight +
|
||||||
|
(custom_titlebar ? kWaylandTitlebarLogicalHeight : 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
SDL_DisplayID DisplayForSlintWindow(const slint::Window& window) {
|
SDL_DisplayID DisplayForSlintWindow(const slint::Window& window) {
|
||||||
const float scale = std::max(window.scale_factor(), 0.01f);
|
const float scale = std::max(window.scale_factor(), 0.01f);
|
||||||
@@ -681,6 +822,8 @@ struct GuiApplication::SlintUi {
|
|||||||
bool portable_service_dialog_initialized = false;
|
bool portable_service_dialog_initialized = false;
|
||||||
std::string recent_model_signature;
|
std::string recent_model_signature;
|
||||||
slint::Timer timer;
|
slint::Timer timer;
|
||||||
|
WindowDragState main_drag;
|
||||||
|
WindowDragState stream_drag;
|
||||||
WindowDragState server_drag;
|
WindowDragState server_drag;
|
||||||
int main_native_titlebar_attempts = 30;
|
int main_native_titlebar_attempts = 30;
|
||||||
int stream_live_resize_configuration_attempts = 0;
|
int stream_live_resize_configuration_attempts = 0;
|
||||||
@@ -696,7 +839,17 @@ struct GuiApplication::SlintUi {
|
|||||||
std::unique_ptr<MacTray> tray;
|
std::unique_ptr<MacTray> tray;
|
||||||
#elif defined(__linux__)
|
#elif defined(__linux__)
|
||||||
std::unique_ptr<LinuxTray> tray;
|
std::unique_ptr<LinuxTray> tray;
|
||||||
|
Display* x11_focus_display = nullptr;
|
||||||
|
std::chrono::steady_clock::time_point last_x11_activation_poll{};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
~SlintUi() {
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
if (x11_focus_display) {
|
||||||
|
XCloseDisplay(x11_focus_display);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
GuiApplication::GuiApplication() = default;
|
GuiApplication::GuiApplication() = default;
|
||||||
@@ -791,6 +944,25 @@ bool GuiApplication::InitializeSDL() {
|
|||||||
if (!getenv("SDL_AUDIODRIVER")) {
|
if (!getenv("SDL_AUDIODRIVER")) {
|
||||||
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
|
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Standard Wayland top-level windows cannot choose an absolute screen
|
||||||
|
// position. Use the session's XWayland compatibility server for the GUI so
|
||||||
|
// auxiliary windows such as the controlled-side status window can be
|
||||||
|
// placed deterministically. Capture and input still detect the surrounding
|
||||||
|
// Wayland session through XDG_SESSION_TYPE and continue to use the portal.
|
||||||
|
const char* requested_video_driver = std::getenv("SDL_VIDEODRIVER");
|
||||||
|
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") &&
|
||||||
|
video_driver_allows_x11;
|
||||||
|
if (use_xwayland_gui_) {
|
||||||
|
setenv("SDL_VIDEODRIVER", "x11", 1);
|
||||||
|
LOG_INFO(
|
||||||
|
"Wayland session detected; using XWayland for positioned GUI "
|
||||||
|
"windows while retaining Wayland portal capture and input");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
|
||||||
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
|
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
|
||||||
@@ -815,10 +987,28 @@ void GuiApplication::InitializeModules() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::InitializeUi() {
|
void GuiApplication::InitializeUi() {
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
// Winit prefers Wayland whenever WAYLAND_DISPLAY/WAYLAND_SOCKET is present.
|
||||||
|
// Hide them only while Slint selects its process-wide window backend, then
|
||||||
|
// restore the environment before the rest of the application starts.
|
||||||
|
std::optional<ScopedUnsetEnvironmentVariable> wayland_display_guard;
|
||||||
|
std::optional<ScopedUnsetEnvironmentVariable> wayland_socket_guard;
|
||||||
|
if (use_xwayland_gui_) {
|
||||||
|
wayland_display_guard.emplace("WAYLAND_DISPLAY");
|
||||||
|
wayland_socket_guard.emplace("WAYLAND_SOCKET");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
ui_ = std::make_unique<SlintUi>();
|
ui_ = std::make_unique<SlintUi>();
|
||||||
|
#if _WIN32
|
||||||
|
ui_->main->set_custom_titlebar(true);
|
||||||
|
#elif defined(__linux__)
|
||||||
|
if (use_xwayland_gui_) {
|
||||||
|
ui_->main->set_custom_titlebar(true);
|
||||||
|
ui_->main->set_wayland_titlebar(true);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
||||||
for (const auto& parsed :
|
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
||||||
ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
|
||||||
ui::ReleaseNoteBlock block;
|
ui::ReleaseNoteBlock block;
|
||||||
block.content = parsed.content;
|
block.content = parsed.content;
|
||||||
block.section_gap = parsed.section_gap;
|
block.section_gap = parsed.section_gap;
|
||||||
@@ -920,6 +1110,9 @@ void GuiApplication::InitializeUi() {
|
|||||||
|
|
||||||
if (ui_->capture_mode && ui_->capture_page == "stream") {
|
if (ui_->capture_mode && ui_->capture_page == "stream") {
|
||||||
ui_->stream.emplace(ui::StreamWindow::create());
|
ui_->stream.emplace(ui::StreamWindow::create());
|
||||||
|
#if defined(__linux__)
|
||||||
|
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
|
||||||
|
#endif
|
||||||
RegisterFontAwesome((*ui_->stream)->window());
|
RegisterFontAwesome((*ui_->stream)->window());
|
||||||
(*ui_->stream)->set_tabs(ui_->tab_model);
|
(*ui_->stream)->set_tabs(ui_->tab_model);
|
||||||
(*ui_->stream)->set_displays(ui_->display_model);
|
(*ui_->stream)->set_displays(ui_->display_model);
|
||||||
@@ -962,9 +1155,10 @@ void GuiApplication::InitializeUi() {
|
|||||||
#endif
|
#endif
|
||||||
BindStreamCallbacks();
|
BindStreamCallbacks();
|
||||||
(*ui_->stream)->show();
|
(*ui_->stream)->show();
|
||||||
PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
|
PositionWindowAtCenter(
|
||||||
kStreamWindowLogicalWidth,
|
(*ui_->stream)->window(), ui_->main->window(),
|
||||||
kStreamWindowLogicalHeight);
|
kStreamWindowLogicalWidth,
|
||||||
|
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()));
|
||||||
ui_->main->hide();
|
ui_->main->hide();
|
||||||
} else if (ui_->capture_mode && ui_->capture_page == "server") {
|
} else if (ui_->capture_mode && ui_->capture_page == "server") {
|
||||||
ui_->server.emplace(ui::ServerWindow::create());
|
ui_->server.emplace(ui::ServerWindow::create());
|
||||||
@@ -1109,6 +1303,27 @@ void GuiApplication::BindMainCallbacks() {
|
|||||||
exit_ = true;
|
exit_ = true;
|
||||||
return slint::CloseRequestResponse::HideWindow;
|
return slint::CloseRequestResponse::HideWindow;
|
||||||
});
|
});
|
||||||
|
main->on_main_title_drag([this](int phase, float x, float y) {
|
||||||
|
if (ui_) {
|
||||||
|
DragWindow(ui_->main, phase, x, y, ui_->main_drag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
main->on_minimize_main_window([this] {
|
||||||
|
if (ui_) {
|
||||||
|
ui_->main->window().set_minimized(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
main->on_close_main_window([this] {
|
||||||
|
if (!ui_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (MinimizeMainWindowToTray()) {
|
||||||
|
ui_->main->hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exit_ = true;
|
||||||
|
ui_->main->hide();
|
||||||
|
});
|
||||||
main->on_copy_local_id([this] {
|
main->on_copy_local_id([this] {
|
||||||
if (!SDL_SetClipboardText(client_id_)) {
|
if (!SDL_SetClipboardText(client_id_)) {
|
||||||
LOG_WARN("Copy local id failed: {}", SDL_GetError());
|
LOG_WARN("Copy local id failed: {}", SDL_GetError());
|
||||||
@@ -1337,7 +1552,7 @@ void GuiApplication::BindStreamCallbacks() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto& stream = *ui_->stream;
|
auto& stream = *ui_->stream;
|
||||||
stream->window().on_close_requested([this] {
|
auto close_stream_sessions = [this] {
|
||||||
std::vector<std::string> ids;
|
std::vector<std::string> ids;
|
||||||
{
|
{
|
||||||
std::shared_lock lock(remote_sessions_mutex_);
|
std::shared_lock lock(remote_sessions_mutex_);
|
||||||
@@ -1348,8 +1563,35 @@ void GuiApplication::BindStreamCallbacks() {
|
|||||||
for (const auto& id : ids) {
|
for (const auto& id : ids) {
|
||||||
CloseStreamTab(id);
|
CloseStreamTab(id);
|
||||||
}
|
}
|
||||||
|
if (ui_ && ui_->stream) {
|
||||||
|
(*ui_->stream)->hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stream->window().on_close_requested([close_stream_sessions] {
|
||||||
|
close_stream_sessions();
|
||||||
return slint::CloseRequestResponse::HideWindow;
|
return slint::CloseRequestResponse::HideWindow;
|
||||||
});
|
});
|
||||||
|
stream->on_stream_title_drag([this](int phase, float x, float y) {
|
||||||
|
if (ui_ && ui_->stream) {
|
||||||
|
DragWindow(*ui_->stream, phase, x, y, ui_->stream_drag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream->on_minimize_stream_window([this] {
|
||||||
|
if (ui_ && ui_->stream) {
|
||||||
|
(*ui_->stream)->window().set_minimized(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream->on_toggle_maximize_stream_window([this] {
|
||||||
|
if (!ui_ || !ui_->stream) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto& stream = *ui_->stream;
|
||||||
|
auto& window = stream->window();
|
||||||
|
const bool maximize = !window.is_maximized();
|
||||||
|
stream->set_window_maximized(maximize);
|
||||||
|
window.set_maximized(maximize);
|
||||||
|
});
|
||||||
|
stream->on_close_stream_window(close_stream_sessions);
|
||||||
stream->on_select_tab([this](int index) { SelectStreamTab(index); });
|
stream->on_select_tab([this](int index) { SelectStreamTab(index); });
|
||||||
stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
|
stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
|
||||||
ReorderStreamTab(from, drop_x, tab_width);
|
ReorderStreamTab(from, drop_x, tab_width);
|
||||||
@@ -1597,6 +1839,9 @@ void GuiApplication::Tick() {
|
|||||||
SyncPlatformDialogs();
|
SyncPlatformDialogs();
|
||||||
SyncStreamWindow();
|
SyncStreamWindow();
|
||||||
SyncServerWindow();
|
SyncServerWindow();
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
SyncXWaylandWindowActivation();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::UpdateLocalization() {
|
void GuiApplication::UpdateLocalization() {
|
||||||
@@ -1782,9 +2027,9 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||||
!defined(__arm__) && USE_CUDA) || \
|
!defined(__arm__) && USE_CUDA) || \
|
||||||
defined(__APPLE__))
|
defined(__APPLE__))
|
||||||
ui_->main->set_hardware_codec_visible(true);
|
ui_->main->set_hardware_codec_available(true);
|
||||||
#else
|
#else
|
||||||
ui_->main->set_hardware_codec_visible(false);
|
ui_->main->set_hardware_codec_available(false);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (show_offline_warning_window_) {
|
if (show_offline_warning_window_) {
|
||||||
@@ -1829,6 +2074,11 @@ void GuiApplication::SyncMainWindow() {
|
|||||||
}
|
}
|
||||||
model.push_back(std::move(item));
|
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));
|
ui_->recent_model->set_vector(std::move(model));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1960,6 +2210,46 @@ void GuiApplication::SyncPlatformDialogs() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
void GuiApplication::SyncXWaylandWindowActivation() {
|
||||||
|
if (!ui_ || !use_xwayland_gui_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (now - ui_->last_x11_activation_poll < 100ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ui_->last_x11_activation_poll = now;
|
||||||
|
|
||||||
|
if (!ui_->x11_focus_display) {
|
||||||
|
ui_->x11_focus_display = XOpenDisplay(nullptr);
|
||||||
|
}
|
||||||
|
Display* display = ui_->x11_focus_display;
|
||||||
|
const ::Window active_window = X11GetActiveWindow(display);
|
||||||
|
if (!active_window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
(*ui_->stream)->set_window_active(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_->main->set_window_active(
|
||||||
|
X11WindowMatches(display, active_window, ui_->main->window()));
|
||||||
|
if (ui_->stream) {
|
||||||
|
(*ui_->stream)
|
||||||
|
->set_window_active(
|
||||||
|
X11WindowMatches(display, active_window, (*ui_->stream)->window()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void GuiApplication::SyncStreamWindow() {
|
void GuiApplication::SyncStreamWindow() {
|
||||||
bool has_sessions = false;
|
bool has_sessions = false;
|
||||||
{
|
{
|
||||||
@@ -1968,6 +2258,9 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
if (need_to_create_stream_window_ && !ui_->stream) {
|
if (need_to_create_stream_window_ && !ui_->stream) {
|
||||||
ui_->stream.emplace(ui::StreamWindow::create());
|
ui_->stream.emplace(ui::StreamWindow::create());
|
||||||
|
#if defined(__linux__)
|
||||||
|
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
|
||||||
|
#endif
|
||||||
RegisterFontAwesome((*ui_->stream)->window());
|
RegisterFontAwesome((*ui_->stream)->window());
|
||||||
// Slint globals belong to a component tree. Force the next localization
|
// Slint globals belong to a component tree. Force the next localization
|
||||||
// pass to initialize the newly-created, independent stream window tree.
|
// pass to initialize the newly-created, independent stream window tree.
|
||||||
@@ -1991,6 +2284,7 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
if (!ui_->stream) {
|
if (!ui_->stream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
|
||||||
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
||||||
@@ -2002,9 +2296,10 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (ui_->stream_initial_position_attempts > 0) {
|
if (ui_->stream_initial_position_attempts > 0) {
|
||||||
if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
|
if (PositionWindowAtCenter(
|
||||||
kStreamWindowLogicalWidth,
|
(*ui_->stream)->window(), ui_->main->window(),
|
||||||
kStreamWindowLogicalHeight)) {
|
kStreamWindowLogicalWidth,
|
||||||
|
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()))) {
|
||||||
--ui_->stream_initial_position_attempts;
|
--ui_->stream_initial_position_attempts;
|
||||||
} else {
|
} else {
|
||||||
ui_->stream_initial_position_attempts = 0;
|
ui_->stream_initial_position_attempts = 0;
|
||||||
@@ -2238,7 +2533,22 @@ void GuiApplication::SyncStreamWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::SyncServerWindow() {
|
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());
|
ui_->server.emplace(ui::ServerWindow::create());
|
||||||
RegisterFontAwesome((*ui_->server)->window());
|
RegisterFontAwesome((*ui_->server)->window());
|
||||||
(*ui_->server)->set_controllers(ui_->controller_model);
|
(*ui_->server)->set_controllers(ui_->controller_model);
|
||||||
@@ -2258,15 +2568,13 @@ void GuiApplication::SyncServerWindow() {
|
|||||||
ui_->server_initial_position_attempts = 2;
|
ui_->server_initial_position_attempts = 2;
|
||||||
server_window_created_ = true;
|
server_window_created_ = true;
|
||||||
server_window_inited_ = 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)->hide();
|
||||||
ui_->server.reset();
|
ui_->server.reset();
|
||||||
server_window_created_ = false;
|
server_window_created_ = false;
|
||||||
server_window_inited_ = false;
|
server_window_inited_ = false;
|
||||||
ui_->server_initial_position_attempts = 0;
|
ui_->server_initial_position_attempts = 0;
|
||||||
need_to_destroy_server_window_ = false;
|
|
||||||
}
|
}
|
||||||
if (!ui_->server) {
|
if (!ui_->server) {
|
||||||
return;
|
return;
|
||||||
@@ -2561,10 +2869,14 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
|
|||||||
const auto size = (*ui_->stream)->window().size();
|
const auto size = (*ui_->stream)->window().size();
|
||||||
const float scale = (*ui_->stream)->window().scale_factor();
|
const float scale = (*ui_->stream)->window().scale_factor();
|
||||||
const float available_width = size.width / scale;
|
const float available_width = size.width / scale;
|
||||||
|
const float titlebar_height =
|
||||||
|
!fullscreen_button_pressed_ && (*ui_->stream)->get_custom_titlebar()
|
||||||
|
? kWaylandTitlebarLogicalHeight
|
||||||
|
: 0.0f;
|
||||||
const float available_height =
|
const float available_height =
|
||||||
size.height / scale - (fullscreen_button_pressed_
|
size.height / scale - titlebar_height -
|
||||||
? 0.0f
|
(fullscreen_button_pressed_ ? 0.0f
|
||||||
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
||||||
float render_width = available_width;
|
float render_width = available_width;
|
||||||
float render_height = available_height;
|
float render_height = available_height;
|
||||||
float offset_x = 0;
|
float offset_x = 0;
|
||||||
@@ -2632,10 +2944,14 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
|
|||||||
const auto size = (*ui_->stream)->window().size();
|
const auto size = (*ui_->stream)->window().size();
|
||||||
const float scale = (*ui_->stream)->window().scale_factor();
|
const float scale = (*ui_->stream)->window().scale_factor();
|
||||||
const float width = size.width / scale;
|
const float width = size.width / scale;
|
||||||
|
const float titlebar_height =
|
||||||
|
!fullscreen_button_pressed_ && (*ui_->stream)->get_custom_titlebar()
|
||||||
|
? kWaylandTitlebarLogicalHeight
|
||||||
|
: 0.0f;
|
||||||
const float height =
|
const float height =
|
||||||
size.height / scale - (fullscreen_button_pressed_
|
size.height / scale - titlebar_height -
|
||||||
? 0.0f
|
(fullscreen_button_pressed_ ? 0.0f
|
||||||
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ private:
|
|||||||
void SyncPlatformDialogs();
|
void SyncPlatformDialogs();
|
||||||
void SyncStreamWindow();
|
void SyncStreamWindow();
|
||||||
void SyncServerWindow();
|
void SyncServerWindow();
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
void SyncXWaylandWindowActivation();
|
||||||
|
#endif
|
||||||
void UpdateLocalization();
|
void UpdateLocalization();
|
||||||
void ResetSettingsUi();
|
void ResetSettingsUi();
|
||||||
void SaveSettingsFromUi();
|
void SaveSettingsFromUi();
|
||||||
@@ -59,6 +62,9 @@ private:
|
|||||||
bool OpenUrl(const std::string &url);
|
bool OpenUrl(const std::string &url);
|
||||||
|
|
||||||
std::unique_ptr<SlintUi> ui_;
|
std::unique_ptr<SlintUi> ui_;
|
||||||
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
|
bool use_xwayland_gui_ = false;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -220,10 +220,8 @@ int GuiApplication::ProcessKeyboardEvent(const SDL_Event &event) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) {
|
// SDL represents key auto-repeat as additional key-down events. Forward
|
||||||
return 0;
|
// them so the fallback path has the same long-press behavior as native hooks.
|
||||||
}
|
|
||||||
|
|
||||||
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
const int key_code = TranslateSdlKeyboardEventToVk(event.key);
|
||||||
if (key_code < 0) {
|
if (key_code < 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -412,4 +410,4 @@ int GuiApplication::ProcessMouseEvent(const SDL_Event &event) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#include "runtime/gui_runtime.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -11,7 +9,9 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "localization.h"
|
#include "localization.h"
|
||||||
|
#include "platform.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ bool IsConnectionAttemptPending(ConnectionStatus status) {
|
|||||||
return status == ConnectionStatus::Connecting ||
|
return status == ConnectionStatus::Connecting ||
|
||||||
status == ConnectionStatus::Gathering;
|
status == ConnectionStatus::Gathering;
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void GuiRuntime::HandleConnectionStatusChange() {
|
void GuiRuntime::HandleConnectionStatusChange() {
|
||||||
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
|
if (signal_connected_ && peer_ && need_to_send_recent_connections_) {
|
||||||
@@ -31,7 +31,7 @@ void GuiRuntime::HandleConnectionStatusChange() {
|
|||||||
j["type"] = "recent_connections_presence";
|
j["type"] = "recent_connections_presence";
|
||||||
j["user_id"] = client_id_;
|
j["user_id"] = client_id_;
|
||||||
j["devices"] = nlohmann::json::array();
|
j["devices"] = nlohmann::json::array();
|
||||||
for (const auto &id : recent_connection_ids_) {
|
for (const auto& id : recent_connection_ids_) {
|
||||||
std::string pure_id = id;
|
std::string pure_id = id;
|
||||||
size_t pos_y = pure_id.find('Y');
|
size_t pos_y = pure_id.find('Y');
|
||||||
size_t pos_n = pure_id.find('N');
|
size_t pos_n = pure_id.find('N');
|
||||||
@@ -123,7 +123,7 @@ void GuiRuntime::HandleConnectionTimeouts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool rejoin_state_changed = false;
|
bool rejoin_state_changed = false;
|
||||||
for (auto &[_, props] : remote_sessions_) {
|
for (auto& [_, props] : remote_sessions_) {
|
||||||
if (!props || !props->connection_attempt_active_.load()) {
|
if (!props || !props->connection_attempt_active_.load()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,7 @@ void GuiRuntime::HandleConnectionTimeouts() {
|
|||||||
|
|
||||||
if (rejoin_state_changed) {
|
if (rejoin_state_changed) {
|
||||||
need_to_rejoin_ = false;
|
need_to_rejoin_ = false;
|
||||||
for (const auto &[_, props] : remote_sessions_) {
|
for (const auto& [_, props] : remote_sessions_) {
|
||||||
if (props && props->rejoin_) {
|
if (props && props->rejoin_) {
|
||||||
need_to_rejoin_ = true;
|
need_to_rejoin_ = true;
|
||||||
break;
|
break;
|
||||||
@@ -161,8 +161,65 @@ void GuiRuntime::HandleConnectionTimeouts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id,
|
void GuiRuntime::HandleServerControllerDisconnected(
|
||||||
const char *password,
|
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) {
|
bool remember_password) {
|
||||||
if (!signal_connected_ || !peer_) {
|
if (!signal_connected_ || !peer_) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -221,7 +278,7 @@ void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
|||||||
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
std::thread save_thread([buffer_copy, video_width, video_height, remote_id,
|
||||||
remote_host_name, password,
|
remote_host_name, password,
|
||||||
thumbnail = thumbnail_]() {
|
thumbnail = thumbnail_]() {
|
||||||
thumbnail->SaveToThumbnail((char *)buffer_copy.data(), video_width,
|
thumbnail->SaveToThumbnail((char*)buffer_copy.data(), video_width,
|
||||||
video_height, remote_id, remote_host_name,
|
video_height, remote_id, remote_host_name,
|
||||||
password);
|
password);
|
||||||
});
|
});
|
||||||
@@ -255,7 +312,7 @@ void GuiRuntime::CloseAllRemoteSessions() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
// std::shared_lock lock(remote_sessions_mutex_);
|
// std::shared_lock lock(remote_sessions_mutex_);
|
||||||
for (auto &it : remote_sessions_) {
|
for (auto& it : remote_sessions_) {
|
||||||
auto props = it.second;
|
auto props = it.second;
|
||||||
CloseRemoteSession(props);
|
CloseRemoteSession(props);
|
||||||
}
|
}
|
||||||
@@ -279,7 +336,7 @@ void GuiRuntime::WaitForThumbnailSaveTasks() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto &thread : threads_to_join) {
|
for (auto& thread : threads_to_join) {
|
||||||
if (thread.joinable()) {
|
if (thread.joinable()) {
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
@@ -300,9 +357,8 @@ void GuiRuntime::ResetRemoteSessionResources(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession>
|
std::shared_ptr<GuiRuntime::RemoteSession> GuiRuntime::FindRemoteSession(
|
||||||
GuiRuntime::FindRemoteSession(
|
const std::string& remote_id) {
|
||||||
const std::string &remote_id) {
|
|
||||||
if (remote_id.empty()) {
|
if (remote_id.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -315,8 +371,7 @@ GuiRuntime::FindRemoteSession(
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int GuiRuntime::ConnectTo(const std::string& remote_id, const char* password,
|
||||||
int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|
||||||
bool remember_password, bool bypass_presence_check) {
|
bool remember_password, bool bypass_presence_check) {
|
||||||
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
if (!bypass_presence_check && !device_presence_cache_.IsOnline(remote_id)) {
|
||||||
int ret =
|
int ret =
|
||||||
@@ -336,19 +391,17 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|||||||
focused_remote_id_ = remote_id;
|
focused_remote_id_ = remote_id;
|
||||||
|
|
||||||
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
// std::shared_lock shared_lock(remote_sessions_mutex_);
|
||||||
bool exists =
|
bool exists = (remote_sessions_.find(remote_id) != remote_sessions_.end());
|
||||||
(remote_sessions_.find(remote_id) != remote_sessions_.end());
|
|
||||||
// shared_lock.unlock();
|
// shared_lock.unlock();
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
PeerPtr *peer_to_init = nullptr;
|
PeerPtr* peer_to_init = nullptr;
|
||||||
std::string local_id;
|
std::string local_id;
|
||||||
|
|
||||||
{
|
{
|
||||||
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
// std::unique_lock unique_lock(remote_sessions_mutex_);
|
||||||
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
if (remote_sessions_.find(remote_id) == remote_sessions_.end()) {
|
||||||
remote_sessions_[remote_id] =
|
remote_sessions_[remote_id] = std::make_shared<RemoteSession>();
|
||||||
std::make_shared<RemoteSession>();
|
|
||||||
auto props = remote_sessions_[remote_id];
|
auto props = remote_sessions_[remote_id];
|
||||||
props->local_id_ = "C-" + std::string(client_id_);
|
props->local_id_ = "C-" + std::string(client_id_);
|
||||||
props->remote_id_ = remote_id;
|
props->remote_id_ = remote_id;
|
||||||
@@ -371,7 +424,7 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto &display_info : devices_.display_info_list()) {
|
for (const auto& display_info : devices_.display_info_list()) {
|
||||||
AddVideoStream(props->peer_, display_info.name.c_str());
|
AddVideoStream(props->peer_, display_info.name.c_str());
|
||||||
}
|
}
|
||||||
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
AddAudioStream(props->peer_, props->audio_label_.c_str());
|
||||||
@@ -429,4 +482,4 @@ int GuiRuntime::ConnectTo(const std::string &remote_id, const char *password,
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -8,16 +8,16 @@
|
|||||||
#include "features/devices/session_device_manager.h"
|
#include "features/devices/session_device_manager.h"
|
||||||
#include "features/file_transfer/file_transfer_manager.h"
|
#include "features/file_transfer/file_transfer_manager.h"
|
||||||
#include "features/input/keyboard_controller.h"
|
#include "features/input/keyboard_controller.h"
|
||||||
|
#include "features/settings/settings_manager.h"
|
||||||
#include "runtime/gui_state.h"
|
#include "runtime/gui_state.h"
|
||||||
#include "runtime/peer_event_handler.h"
|
#include "runtime/peer_event_handler.h"
|
||||||
#include "features/settings/settings_manager.h"
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
// Shared GUI runtime. It owns subsystem controllers and cross-cutting session
|
||||||
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
// state, but no window lifecycle, ImGui view, or transport callback methods.
|
||||||
class GuiRuntime : protected gui_detail::GuiState {
|
class GuiRuntime : protected gui_detail::GuiState {
|
||||||
protected:
|
protected:
|
||||||
using FileTransferState = gui_detail::FileTransferState;
|
using FileTransferState = gui_detail::FileTransferState;
|
||||||
using RemoteSession = gui_detail::RemoteSession;
|
using RemoteSession = gui_detail::RemoteSession;
|
||||||
|
|
||||||
@@ -32,35 +32,35 @@ protected:
|
|||||||
GuiRuntime();
|
GuiRuntime();
|
||||||
~GuiRuntime();
|
~GuiRuntime();
|
||||||
|
|
||||||
static void SdlCaptureAudioIn(void *userdata, Uint8 *stream, int len);
|
static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len);
|
||||||
static void SdlCaptureAudioOut(void *userdata, Uint8 *stream, int len);
|
static void SdlCaptureAudioOut(void* userdata, Uint8* stream, int len);
|
||||||
|
|
||||||
int CreateConnectionPeer();
|
int CreateConnectionPeer();
|
||||||
int ConnectTo(const std::string &remote_id, const char *password,
|
int ConnectTo(const std::string& remote_id, const char* password,
|
||||||
bool remember_password, bool bypass_presence_check = false);
|
bool remember_password, bool bypass_presence_check = false);
|
||||||
int RequestSingleDevicePresence(const std::string &remote_id,
|
int RequestSingleDevicePresence(const std::string& remote_id,
|
||||||
const char *password, bool remember_password);
|
const char* password, bool remember_password);
|
||||||
|
|
||||||
void UpdateLabels();
|
void UpdateLabels();
|
||||||
void HandleRecentConnections();
|
void HandleRecentConnections();
|
||||||
void HandleConnectionStatusChange();
|
void HandleConnectionStatusChange();
|
||||||
void HandlePendingPresenceProbe();
|
void HandlePendingPresenceProbe();
|
||||||
void HandleConnectionTimeouts();
|
void HandleConnectionTimeouts();
|
||||||
|
void HandleServerControllerDisconnected(const std::string& remote_id,
|
||||||
|
const char* reason);
|
||||||
void HandleWindowsServiceIntegration();
|
void HandleWindowsServiceIntegration();
|
||||||
|
|
||||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||||
void CloseAllRemoteSessions();
|
void CloseAllRemoteSessions();
|
||||||
void ResetRemoteSessionResources(
|
void ResetRemoteSessionResources(std::shared_ptr<RemoteSession> props);
|
||||||
std::shared_ptr<RemoteSession> props);
|
|
||||||
void WaitForThumbnailSaveTasks();
|
void WaitForThumbnailSaveTasks();
|
||||||
std::shared_ptr<RemoteSession>
|
std::shared_ptr<RemoteSession> FindRemoteSession(
|
||||||
FindRemoteSession(const std::string &remote_id);
|
const std::string& remote_id);
|
||||||
|
|
||||||
void ResetRemoteServiceStatus(RemoteSession &props);
|
void ResetRemoteServiceStatus(RemoteSession& props);
|
||||||
void ApplyRemoteServiceStatus(RemoteSession &props,
|
void ApplyRemoteServiceStatus(RemoteSession& props,
|
||||||
const ServiceStatus &status);
|
const ServiceStatus& status);
|
||||||
RemoteUnlockState
|
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
|
||||||
GetRemoteUnlockState(const RemoteSession &props) const;
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||||
#endif
|
#endif
|
||||||
@@ -82,7 +82,7 @@ protected:
|
|||||||
KeyboardController keyboard_;
|
KeyboardController keyboard_;
|
||||||
PeerEventHandler peer_events_;
|
PeerEventHandler peer_events_;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class ClipboardController;
|
friend class ClipboardController;
|
||||||
friend class SessionDeviceManager;
|
friend class SessionDeviceManager;
|
||||||
friend class FileTransferManager;
|
friend class FileTransferManager;
|
||||||
@@ -91,6 +91,6 @@ private:
|
|||||||
friend class PeerEventHandler;
|
friend class PeerEventHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif // CROSSDESK_GUI_RUNTIME_H_
|
#endif // CROSSDESK_GUI_RUNTIME_H_
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
PeerEventHandler::PeerEventHandler(GuiRuntime &owner) : owner_(owner) {}
|
PeerEventHandler::PeerEventHandler(GuiRuntime& owner) : owner_(owner) {}
|
||||||
|
|
||||||
void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
|
||||||
void *user_data) {
|
void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime || !message || size == 0) {
|
if (!runtime || !message || size == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
|||||||
std::string type = j["type"].get<std::string>();
|
std::string type = j["type"].get<std::string>();
|
||||||
if (type == "presence") {
|
if (type == "presence") {
|
||||||
if (j.contains("devices") && j["devices"].is_array()) {
|
if (j.contains("devices") && j["devices"].is_array()) {
|
||||||
for (auto &dev : j["devices"]) {
|
for (auto& dev : j["devices"]) {
|
||||||
if (!dev.is_object()) {
|
if (!dev.is_object()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -88,11 +88,10 @@ void PeerEventHandler::OnSignalMessage(const char *message, size_t size,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char* user_id,
|
||||||
void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
|
size_t user_id_size, void* user_data) {
|
||||||
size_t user_id_size, void *user_data) {
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
|
||||||
if (!runtime) {
|
if (!runtime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -150,13 +149,12 @@ void PeerEventHandler::OnSignalStatus(SignalStatus status, const char *user_id,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||||
const char *user_id,
|
const char* user_id,
|
||||||
const size_t user_id_size,
|
const size_t user_id_size,
|
||||||
void *user_data) {
|
void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime)
|
if (!runtime) return;
|
||||||
return;
|
|
||||||
|
|
||||||
std::string remote_id(user_id, user_id_size);
|
std::string remote_id(user_id, user_id_size);
|
||||||
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
std::shared_ptr<GuiRuntime::RemoteSession> props;
|
||||||
@@ -178,107 +176,110 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
{
|
{
|
||||||
RemoteAction remote_action;
|
RemoteAction remote_action;
|
||||||
remote_action.i.display_num =
|
remote_action.i.display_num =
|
||||||
runtime->devices_.display_info_list().size();
|
runtime->devices_.display_info_list().size();
|
||||||
remote_action.i.display_list =
|
remote_action.i.display_list =
|
||||||
(char **)malloc(remote_action.i.display_num * sizeof(char *));
|
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||||
remote_action.i.left =
|
remote_action.i.left =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
remote_action.i.top =
|
remote_action.i.top =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
remote_action.i.right =
|
remote_action.i.right =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
remote_action.i.bottom =
|
remote_action.i.bottom =
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
runtime->devices_.display_info_list()[i].name);
|
runtime->devices_.display_info_list()[i].name);
|
||||||
remote_action.i.display_list[i] = (char *)malloc(
|
remote_action.i.display_list[i] = (char*)malloc(
|
||||||
runtime->devices_.display_info_list()[i].name.length() + 1);
|
runtime->devices_.display_info_list()[i].name.length() + 1);
|
||||||
strncpy(remote_action.i.display_list[i],
|
strncpy(remote_action.i.display_list[i],
|
||||||
runtime->devices_.display_info_list()[i].name.c_str(),
|
runtime->devices_.display_info_list()[i].name.c_str(),
|
||||||
runtime->devices_.display_info_list()[i].name.length());
|
runtime->devices_.display_info_list()[i].name.length());
|
||||||
remote_action.i.display_list
|
remote_action.i
|
||||||
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
|
.display_list[i][runtime->devices_.display_info_list()[i]
|
||||||
remote_action.i.left[i] =
|
.name.length()] = '\0';
|
||||||
runtime->devices_.display_info_list()[i].left;
|
remote_action.i.left[i] =
|
||||||
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
|
runtime->devices_.display_info_list()[i].left;
|
||||||
remote_action.i.right[i] =
|
remote_action.i.top[i] =
|
||||||
runtime->devices_.display_info_list()[i].right;
|
runtime->devices_.display_info_list()[i].top;
|
||||||
remote_action.i.bottom[i] =
|
remote_action.i.right[i] =
|
||||||
runtime->devices_.display_info_list()[i].bottom;
|
runtime->devices_.display_info_list()[i].right;
|
||||||
|
remote_action.i.bottom[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string host_name = GetHostName();
|
||||||
|
remote_action.type = ControlType::host_infomation;
|
||||||
|
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||||
|
host_name.size());
|
||||||
|
remote_action.i.host_name[host_name.size()] = '\0';
|
||||||
|
remote_action.i.host_name_size = host_name.size();
|
||||||
|
|
||||||
|
std::string msg = remote_action.to_json();
|
||||||
|
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
||||||
|
runtime->control_data_label_.c_str());
|
||||||
|
remote_action_codec::Free(remote_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string host_name = GetHostName();
|
if (!runtime->need_to_create_stream_window_ &&
|
||||||
remote_action.type = ControlType::host_infomation;
|
!runtime->remote_sessions_.empty()) {
|
||||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
runtime->need_to_create_stream_window_ = true;
|
||||||
remote_action.i.host_name[host_name.size()] = '\0';
|
}
|
||||||
remote_action.i.host_name_size = host_name.size();
|
props->connection_established_ = true;
|
||||||
|
runtime->start_keyboard_capturer_ = true;
|
||||||
std::string msg = remote_action.to_json();
|
break;
|
||||||
int ret = SendReliableDataFrame(props->peer_, msg.data(), msg.size(),
|
|
||||||
runtime->control_data_label_.c_str());
|
|
||||||
remote_action_codec::Free(remote_action);
|
|
||||||
}
|
}
|
||||||
|
case ConnectionStatus::Disconnected:
|
||||||
if (!runtime->need_to_create_stream_window_ &&
|
case ConnectionStatus::Failed:
|
||||||
!runtime->remote_sessions_.empty()) {
|
case ConnectionStatus::Closed: {
|
||||||
runtime->need_to_create_stream_window_ = true;
|
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
||||||
}
|
"connection_closed");
|
||||||
props->connection_established_ = true;
|
|
||||||
runtime->start_keyboard_capturer_ = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ConnectionStatus::Disconnected:
|
|
||||||
case ConnectionStatus::Failed:
|
|
||||||
case ConnectionStatus::Closed: {
|
|
||||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
|
||||||
"connection_closed");
|
|
||||||
props->connection_established_ = false;
|
|
||||||
props->enable_mouse_control_ = false;
|
|
||||||
runtime->ResetRemoteServiceStatus(*props);
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
|
||||||
props->front_frame_.reset();
|
|
||||||
props->back_frame_.reset();
|
|
||||||
props->video_width_ = 0;
|
|
||||||
props->video_height_ = 0;
|
|
||||||
props->video_size_ = 0;
|
|
||||||
props->render_rect_dirty_ = true;
|
|
||||||
props->stream_cleanup_pending_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime->focus_on_stream_window_ = false;
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ConnectionStatus::IncorrectPassword: {
|
|
||||||
runtime->password_validating_ = false;
|
|
||||||
runtime->password_validating_time_++;
|
|
||||||
if (runtime->connect_button_pressed_) {
|
|
||||||
runtime->connect_button_pressed_ = false;
|
|
||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
runtime->connect_button_label_ =
|
props->enable_mouse_control_ = false;
|
||||||
localization::connect[runtime->localization_language_index_];
|
runtime->ResetRemoteServiceStatus(*props);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||||
|
props->front_frame_.reset();
|
||||||
|
props->back_frame_.reset();
|
||||||
|
props->video_width_ = 0;
|
||||||
|
props->video_height_ = 0;
|
||||||
|
props->video_size_ = 0;
|
||||||
|
props->render_rect_dirty_ = true;
|
||||||
|
props->stream_cleanup_pending_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime->focus_on_stream_window_ = false;
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
case ConnectionStatus::IncorrectPassword: {
|
||||||
}
|
runtime->password_validating_ = false;
|
||||||
case ConnectionStatus::NoSuchTransmissionId:
|
runtime->password_validating_time_++;
|
||||||
case ConnectionStatus::RemoteUnavailable: {
|
if (runtime->connect_button_pressed_) {
|
||||||
if (runtime->connect_button_pressed_) {
|
runtime->connect_button_pressed_ = false;
|
||||||
props->connection_established_ = false;
|
props->connection_established_ = false;
|
||||||
runtime->connect_button_label_ =
|
runtime->connect_button_label_ =
|
||||||
localization::connect[runtime->localization_language_index_];
|
localization::connect[runtime->localization_language_index_];
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
case ConnectionStatus::NoSuchTransmissionId:
|
||||||
}
|
case ConnectionStatus::RemoteUnavailable: {
|
||||||
default:
|
if (runtime->connect_button_pressed_) {
|
||||||
break;
|
props->connection_established_ = false;
|
||||||
|
runtime->connect_button_label_ =
|
||||||
|
localization::connect[runtime->localization_language_index_];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runtime->is_client_mode_ = false;
|
runtime->is_client_mode_ = false;
|
||||||
@@ -289,155 +290,107 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus::Connected: {
|
case ConnectionStatus::Connected: {
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
runtime->last_windows_service_status_tick_ = 0;
|
runtime->last_windows_service_status_tick_ = 0;
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
RemoteAction remote_action;
|
|
||||||
remote_action.i.display_num =
|
|
||||||
runtime->devices_.display_info_list().size();
|
|
||||||
remote_action.i.display_list =
|
|
||||||
(char **)malloc(remote_action.i.display_num * sizeof(char *));
|
|
||||||
remote_action.i.left =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
remote_action.i.top =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
remote_action.i.right =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
remote_action.i.bottom =
|
|
||||||
(int *)malloc(remote_action.i.display_num * sizeof(int));
|
|
||||||
for (int i = 0; i < remote_action.i.display_num; i++) {
|
|
||||||
LOG_INFO("Local display [{}:{}]", i + 1,
|
|
||||||
runtime->devices_.display_info_list()[i].name);
|
|
||||||
remote_action.i.display_list[i] = (char *)malloc(
|
|
||||||
runtime->devices_.display_info_list()[i].name.length() + 1);
|
|
||||||
strncpy(remote_action.i.display_list[i],
|
|
||||||
runtime->devices_.display_info_list()[i].name.c_str(),
|
|
||||||
runtime->devices_.display_info_list()[i].name.length());
|
|
||||||
remote_action.i.display_list
|
|
||||||
[i][runtime->devices_.display_info_list()[i].name.length()] = '\0';
|
|
||||||
remote_action.i.left[i] =
|
|
||||||
runtime->devices_.display_info_list()[i].left;
|
|
||||||
remote_action.i.top[i] = runtime->devices_.display_info_list()[i].top;
|
|
||||||
remote_action.i.right[i] =
|
|
||||||
runtime->devices_.display_info_list()[i].right;
|
|
||||||
remote_action.i.bottom[i] =
|
|
||||||
runtime->devices_.display_info_list()[i].bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string host_name = GetHostName();
|
|
||||||
remote_action.type = ControlType::host_infomation;
|
|
||||||
memcpy(&remote_action.i.host_name, host_name.data(), host_name.size());
|
|
||||||
remote_action.i.host_name[host_name.size()] = '\0';
|
|
||||||
remote_action.i.host_name_size = host_name.size();
|
|
||||||
|
|
||||||
std::string msg = remote_action.to_json();
|
|
||||||
int ret = SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
|
|
||||||
runtime->control_data_label_.c_str());
|
|
||||||
remote_action_codec::Free(remote_action);
|
|
||||||
}
|
|
||||||
|
|
||||||
runtime->need_to_create_server_window_ = true;
|
|
||||||
runtime->is_server_mode_ = true;
|
|
||||||
runtime->start_screen_capturer_ = true;
|
|
||||||
runtime->start_speaker_capturer_ = true;
|
|
||||||
runtime->remote_client_id_ = remote_id;
|
|
||||||
runtime->start_mouse_controller_ = true;
|
|
||||||
{
|
|
||||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
|
||||||
if (std::all_of(runtime->connection_status_.begin(),
|
|
||||||
runtime->connection_status_.end(), [](const auto &kv) {
|
|
||||||
return kv.first.find("web") != std::string::npos;
|
|
||||||
})) {
|
|
||||||
runtime->show_cursor_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ConnectionStatus::Disconnected:
|
|
||||||
case ConnectionStatus::Failed:
|
|
||||||
case ConnectionStatus::Closed: {
|
|
||||||
runtime->keyboard_.ReleaseRemotePressedKeys(remote_id,
|
|
||||||
"connection_closed");
|
|
||||||
bool all_disconnected = false;
|
|
||||||
{
|
|
||||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
|
||||||
all_disconnected =
|
|
||||||
std::all_of(runtime->connection_status_.begin(),
|
|
||||||
runtime->connection_status_.end(), [](const auto &kv) {
|
|
||||||
return kv.second == ConnectionStatus::Closed ||
|
|
||||||
kv.second == ConnectionStatus::Failed ||
|
|
||||||
kv.second == ConnectionStatus::Disconnected;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (all_disconnected) {
|
|
||||||
runtime->need_to_destroy_server_window_ = true;
|
|
||||||
runtime->is_server_mode_ = false;
|
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
|
||||||
if (IsWaylandSession()) {
|
|
||||||
// Keep Wayland capture session warm to avoid black screen on
|
|
||||||
// subsequent reconnects.
|
|
||||||
runtime->start_screen_capturer_ = true;
|
|
||||||
LOG_INFO("Keeping Wayland screen capturer running after "
|
|
||||||
"disconnect to preserve reconnect stability");
|
|
||||||
} else {
|
|
||||||
runtime->start_screen_capturer_ = false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
runtime->start_screen_capturer_ = false;
|
|
||||||
#endif
|
|
||||||
runtime->start_speaker_capturer_ = false;
|
|
||||||
runtime->start_mouse_controller_ = false;
|
|
||||||
runtime->start_keyboard_capturer_ = false;
|
|
||||||
runtime->remote_client_id_ = "";
|
|
||||||
if (props)
|
|
||||||
props->connection_established_ = false;
|
|
||||||
if (runtime->audio_capture_) {
|
|
||||||
runtime->devices_.StopSpeakerCapturer();
|
|
||||||
runtime->audio_capture_ = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
RemoteAction remote_action;
|
||||||
runtime->connection_status_.erase(remote_id);
|
remote_action.i.display_num =
|
||||||
runtime->connection_host_names_.erase(remote_id);
|
runtime->devices_.display_info_list().size();
|
||||||
}
|
remote_action.i.display_list =
|
||||||
runtime->devices_.ResetToInitialDisplay();
|
(char**)malloc(remote_action.i.display_num * sizeof(char*));
|
||||||
}
|
remote_action.i.left =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
remote_action.i.top =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
remote_action.i.right =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
remote_action.i.bottom =
|
||||||
|
(int*)malloc(remote_action.i.display_num * sizeof(int));
|
||||||
|
for (int i = 0; i < remote_action.i.display_num; i++) {
|
||||||
|
LOG_INFO("Local display [{}:{}]", i + 1,
|
||||||
|
runtime->devices_.display_info_list()[i].name);
|
||||||
|
remote_action.i.display_list[i] = (char*)malloc(
|
||||||
|
runtime->devices_.display_info_list()[i].name.length() + 1);
|
||||||
|
strncpy(remote_action.i.display_list[i],
|
||||||
|
runtime->devices_.display_info_list()[i].name.c_str(),
|
||||||
|
runtime->devices_.display_info_list()[i].name.length());
|
||||||
|
remote_action.i
|
||||||
|
.display_list[i][runtime->devices_.display_info_list()[i]
|
||||||
|
.name.length()] = '\0';
|
||||||
|
remote_action.i.left[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].left;
|
||||||
|
remote_action.i.top[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].top;
|
||||||
|
remote_action.i.right[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].right;
|
||||||
|
remote_action.i.bottom[i] =
|
||||||
|
runtime->devices_.display_info_list()[i].bottom;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
std::string host_name = GetHostName();
|
||||||
std::shared_lock lock(runtime->connection_status_mutex_);
|
remote_action.type = ControlType::host_infomation;
|
||||||
if (std::all_of(runtime->connection_status_.begin(),
|
memcpy(&remote_action.i.host_name, host_name.data(),
|
||||||
runtime->connection_status_.end(), [](const auto &kv) {
|
host_name.size());
|
||||||
return kv.first.find("web") == std::string::npos;
|
remote_action.i.host_name[host_name.size()] = '\0';
|
||||||
})) {
|
remote_action.i.host_name_size = host_name.size();
|
||||||
runtime->show_cursor_ = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
std::string msg = remote_action.to_json();
|
||||||
}
|
int ret =
|
||||||
default:
|
SendReliableDataFrame(runtime->peer_, msg.data(), msg.size(),
|
||||||
break;
|
runtime->control_data_label_.c_str());
|
||||||
|
remote_action_codec::Free(remote_action);
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime->need_to_destroy_server_window_.store(
|
||||||
|
false, std::memory_order_release);
|
||||||
|
runtime->need_to_create_server_window_.store(true,
|
||||||
|
std::memory_order_release);
|
||||||
|
runtime->is_server_mode_ = true;
|
||||||
|
runtime->start_screen_capturer_ = true;
|
||||||
|
runtime->start_speaker_capturer_ = true;
|
||||||
|
runtime->remote_client_id_ = remote_id;
|
||||||
|
runtime->start_mouse_controller_ = true;
|
||||||
|
{
|
||||||
|
std::shared_lock lock(runtime->connection_status_mutex_);
|
||||||
|
if (std::all_of(runtime->connection_status_.begin(),
|
||||||
|
runtime->connection_status_.end(),
|
||||||
|
[](const auto& kv) {
|
||||||
|
return kv.first.find("web") != std::string::npos;
|
||||||
|
})) {
|
||||||
|
runtime->show_cursor_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ConnectionStatus::Disconnected:
|
||||||
|
case ConnectionStatus::Failed:
|
||||||
|
case ConnectionStatus::Closed: {
|
||||||
|
runtime->HandleServerControllerDisconnected(remote_id,
|
||||||
|
"connection_closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PeerEventHandler::OnNetStatusReport(
|
void PeerEventHandler::OnNetStatusReport(
|
||||||
const char *client_id, size_t client_id_size, TraversalMode mode,
|
const char* client_id, size_t client_id_size, TraversalMode mode,
|
||||||
const XNetTrafficStats *net_traffic_stats, const char *user_id,
|
const XNetTrafficStats* net_traffic_stats, const char* user_id,
|
||||||
const size_t user_id_size, void *user_data) {
|
const size_t user_id_size, void* user_data) {
|
||||||
auto *handler = static_cast<PeerEventHandler *>(user_data);
|
auto* handler = static_cast<PeerEventHandler*>(user_data);
|
||||||
GuiRuntime *runtime = handler ? &handler->owner_ : nullptr;
|
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
|
||||||
if (!runtime) {
|
if (!runtime) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
if (strchr(client_id, '@') != nullptr && strchr(user_id, '-') == nullptr) {
|
||||||
std::string id, password;
|
std::string id, password;
|
||||||
const char *at_pos = strchr(client_id, '@');
|
const char* at_pos = strchr(client_id, '@');
|
||||||
if (at_pos == nullptr) {
|
if (at_pos == nullptr) {
|
||||||
id = client_id;
|
id = client_id;
|
||||||
password.clear();
|
password.clear();
|
||||||
@@ -510,4 +463,4 @@ void PeerEventHandler::OnNetStatusReport(
|
|||||||
props->net_traffic_stats_ = *net_traffic_stats;
|
props->net_traffic_stats_ = *net_traffic_stats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ export component ImGuiCheckBox inherits Rectangle {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
if root.checked: Text {
|
if root.checked: Text {
|
||||||
text: FontAwesomeIcons.check;
|
text: FontAwesomeIcons.check;
|
||||||
color: #2e86de;
|
color: root.enabled ? #2e86de : #8b8f95;
|
||||||
font-family: "Font Awesome 6 Free";
|
font-family: "Font Awesome 6 Free";
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
+317
-63
@@ -184,15 +184,21 @@ component ConnectionDialogButton inherits Rectangle {
|
|||||||
export component MainWindow inherits Window {
|
export component MainWindow inherits Window {
|
||||||
title: "CrossDesk";
|
title: "CrossDesk";
|
||||||
preferred-width: 640px;
|
preferred-width: 640px;
|
||||||
preferred-height: 450px;
|
preferred-height: 450px + root.titlebar-height;
|
||||||
min-width: 640px;
|
min-width: 640px;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
min-height: 450px;
|
min-height: 450px + root.titlebar-height;
|
||||||
max-height: 450px;
|
max-height: 450px + root.titlebar-height;
|
||||||
no-frame: false;
|
no-frame: root.custom-titlebar;
|
||||||
background: white;
|
background: root.wayland-titlebar ? transparent : white;
|
||||||
default-font-size: ImGuiFontStyle.base;
|
default-font-size: ImGuiFontStyle.base;
|
||||||
|
|
||||||
|
in property <bool> custom-titlebar: false;
|
||||||
|
in property <bool> wayland-titlebar: false;
|
||||||
|
in property <bool> window-active: true;
|
||||||
|
private property <length> titlebar-height:
|
||||||
|
root.custom-titlebar ? (root.wayland-titlebar ? 32px : 31px) : 0px;
|
||||||
|
private property <length> wayland-corner-radius: 12px;
|
||||||
in property <string> local-id: "";
|
in property <string> local-id: "";
|
||||||
in property <string> local-password: "";
|
in property <string> local-password: "";
|
||||||
in-out property <string> remote-id-input: "";
|
in-out property <string> remote-id-input: "";
|
||||||
@@ -242,7 +248,9 @@ export component MainWindow inherits Window {
|
|||||||
in-out property <string> server-port: "";
|
in-out property <string> server-port: "";
|
||||||
in-out property <string> coturn-port: "";
|
in-out property <string> coturn-port: "";
|
||||||
in property <bool> settings-session-active: false;
|
in property <bool> settings-session-active: false;
|
||||||
in property <bool> hardware-codec-visible: true;
|
// Keep the setting visible on every platform. Unsupported builds expose it
|
||||||
|
// as disabled so the fixed settings layout never turns into a blank row.
|
||||||
|
in property <bool> hardware-codec-available: true;
|
||||||
|
|
||||||
callback copy-local-id;
|
callback copy-local-id;
|
||||||
callback toggle-password-visibility;
|
callback toggle-password-visibility;
|
||||||
@@ -268,9 +276,13 @@ export component MainWindow inherits Window {
|
|||||||
callback cancel-portable-service;
|
callback cancel-portable-service;
|
||||||
callback acknowledge-portable-service;
|
callback acknowledge-portable-service;
|
||||||
callback acknowledge-portable-service-suppressed;
|
callback acknowledge-portable-service-suppressed;
|
||||||
|
callback main-title-drag(int, length, length);
|
||||||
|
callback minimize-main-window;
|
||||||
|
callback close-main-window;
|
||||||
|
|
||||||
private property <bool> menu-open: false;
|
private property <bool> menu-open: false;
|
||||||
in-out property <bool> settings-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> self-host-settings-open: false;
|
||||||
in-out property <bool> about-open: false;
|
in-out property <bool> about-open: false;
|
||||||
in-out property <bool> update-open: root.update-available;
|
in-out property <bool> update-open: root.update-available;
|
||||||
@@ -304,17 +316,216 @@ export component MainWindow inherits Window {
|
|||||||
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
|
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
|
||||||
: root.recent-tooltip-pointer-x + 12px));
|
: root.recent-tooltip-pointer-x + 12px));
|
||||||
private property <length> recent-tooltip-y: min(
|
private property <length> recent-tooltip-y: min(
|
||||||
root.height - root.recent-tooltip-height - 8px,
|
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
|
||||||
max(8px, root.recent-tooltip-pointer-y - 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 => {
|
reset-remote-id => {
|
||||||
root.remote-id-input = "";
|
root.remote-id-input = "";
|
||||||
remote-id-editor.focus();
|
remote-id-editor.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
window-surface := Rectangle {
|
||||||
width: root.width;
|
width: root.width;
|
||||||
height: root.height;
|
height: root.height;
|
||||||
|
background: root.wayland-titlebar ? #f6f5f4 : transparent;
|
||||||
|
border-radius:
|
||||||
|
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
|
||||||
|
clip: root.wayland-titlebar;
|
||||||
|
|
||||||
|
if root.custom-titlebar && root.wayland-titlebar: Rectangle {
|
||||||
|
x: 0px;
|
||||||
|
y: 0px;
|
||||||
|
width: parent.width;
|
||||||
|
height: root.titlebar-height;
|
||||||
|
background: #f6f5f4;
|
||||||
|
z: 100;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 80px;
|
||||||
|
width: parent.width - 160px;
|
||||||
|
height: parent.height;
|
||||||
|
text: "CrossDesk";
|
||||||
|
color: root.window-active ? #3d3d3d : #747474;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
wayland-title-drag := TouchArea {
|
||||||
|
width: parent.width - 80px;
|
||||||
|
property <int> phase: self.pressed ? 1 : 0;
|
||||||
|
changed phase => {
|
||||||
|
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
|
||||||
|
}
|
||||||
|
moved => {
|
||||||
|
if (self.pressed) {
|
||||||
|
root.main-title-drag(2, self.mouse-x, self.mouse-y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wayland-minimize-button := Rectangle {
|
||||||
|
x: parent.width - 74px;
|
||||||
|
y: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: wayland-minimize-touch.pressed ? #c0bfbc
|
||||||
|
: wayland-minimize-touch.has-hover ? #deddda
|
||||||
|
: root.window-active ? #e6e6e6
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: "Minimize";
|
||||||
|
accessible-action-default => { wayland-minimize-touch.clicked(); }
|
||||||
|
Rectangle {
|
||||||
|
x: 8px;
|
||||||
|
y: 13px;
|
||||||
|
width: 8px;
|
||||||
|
height: 0.75px;
|
||||||
|
background: root.window-active
|
||||||
|
|| wayland-minimize-touch.has-hover
|
||||||
|
|| wayland-minimize-touch.pressed ? #454545 : #898989;
|
||||||
|
}
|
||||||
|
wayland-minimize-touch := TouchArea {
|
||||||
|
clicked => { root.minimize-main-window(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wayland-close-button := Rectangle {
|
||||||
|
x: parent.width - 34px;
|
||||||
|
y: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: wayland-close-touch.pressed ? #b2161d
|
||||||
|
: wayland-close-touch.has-hover ? #e01b24
|
||||||
|
: root.window-active ? #e6e6e6
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: "Close";
|
||||||
|
accessible-action-default => { wayland-close-touch.clicked(); }
|
||||||
|
Path {
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height;
|
||||||
|
viewbox-width: 24;
|
||||||
|
viewbox-height: 24;
|
||||||
|
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
|
||||||
|
stroke: wayland-close-touch.has-hover
|
||||||
|
|| wayland-close-touch.pressed ? white
|
||||||
|
: root.window-active ? #454545 : #898989;
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
wayland-close-touch := TouchArea {
|
||||||
|
clicked => { root.close-main-window(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
y: parent.height - 1px;
|
||||||
|
height: 1px;
|
||||||
|
background: #d5d3d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.custom-titlebar && !root.wayland-titlebar: Rectangle {
|
||||||
|
x: 0px;
|
||||||
|
y: 0px;
|
||||||
|
width: root.width;
|
||||||
|
height: 31px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
z: 100;
|
||||||
|
|
||||||
|
Image {
|
||||||
|
x: 8px;
|
||||||
|
y: 7px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
source: @image-url("../../../icons/linux/crossdesk_16x16.png");
|
||||||
|
image-fit: contain;
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
x: 30px;
|
||||||
|
width: parent.width - 122px;
|
||||||
|
height: parent.height;
|
||||||
|
text: "CrossDesk";
|
||||||
|
color: #202124;
|
||||||
|
font-size: 12px;
|
||||||
|
vertical-alignment: center;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
title-drag := TouchArea {
|
||||||
|
x: 0px;
|
||||||
|
width: parent.width - 92px;
|
||||||
|
property <int> phase: self.pressed ? 1 : 0;
|
||||||
|
changed phase => { root.main-title-drag(phase, self.mouse-x, self.mouse-y); }
|
||||||
|
moved => { if (self.pressed) { root.main-title-drag(2, self.mouse-x, self.mouse-y); } }
|
||||||
|
}
|
||||||
|
minimize-button := Rectangle {
|
||||||
|
x: parent.width - 92px;
|
||||||
|
width: 46px;
|
||||||
|
height: parent.height;
|
||||||
|
background: minimize-touch.pressed ? #c7c7c7
|
||||||
|
: minimize-touch.has-hover ? #e5e5e5
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: "Minimize";
|
||||||
|
accessible-action-default => { minimize-touch.clicked(); }
|
||||||
|
Text {
|
||||||
|
text: FontAwesomeIcons.minus;
|
||||||
|
color: #202124;
|
||||||
|
font-family: "Font Awesome 6 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 10px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
accessible-role: none;
|
||||||
|
}
|
||||||
|
minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } }
|
||||||
|
}
|
||||||
|
close-button := Rectangle {
|
||||||
|
x: parent.width - 46px;
|
||||||
|
width: 46px;
|
||||||
|
height: parent.height;
|
||||||
|
background: close-touch.pressed ? #b3251b
|
||||||
|
: close-touch.has-hover ? #c42b1c
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: "Close";
|
||||||
|
accessible-action-default => { close-touch.clicked(); }
|
||||||
|
Text {
|
||||||
|
text: FontAwesomeIcons.xmark;
|
||||||
|
color: close-touch.has-hover || close-touch.pressed ? white : #202124;
|
||||||
|
font-family: "Font Awesome 6 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 12px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
accessible-role: none;
|
||||||
|
}
|
||||||
|
close-touch := TouchArea { clicked => { root.close-main-window(); } }
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
y: parent.height - 1px;
|
||||||
|
height: 1px;
|
||||||
|
background: #d4d4d4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content-layer := Rectangle {
|
||||||
|
x: 0px;
|
||||||
|
y: root.titlebar-height;
|
||||||
|
width: root.width;
|
||||||
|
height: root.height - self.y;
|
||||||
|
clip: true;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height;
|
||||||
background: white;
|
background: white;
|
||||||
border-width: 0px;
|
border-width: 0px;
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
@@ -334,8 +545,8 @@ export component MainWindow inherits Window {
|
|||||||
z: 7;
|
z: 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// App actions live in the content header; native window controls and
|
// App actions live in the content header; window controls and dragging
|
||||||
// dragging are provided by the system title bar.
|
// remain in the native or Windows-specific title bar above it.
|
||||||
menu-button := IconButton {
|
menu-button := IconButton {
|
||||||
x: parent.width - 40px;
|
x: parent.width - 40px;
|
||||||
y: 4px;
|
y: 4px;
|
||||||
@@ -780,8 +991,8 @@ export component MainWindow inherits Window {
|
|||||||
if root.menu-open: TouchArea {
|
if root.menu-open: TouchArea {
|
||||||
x: 0px;
|
x: 0px;
|
||||||
y: 0px;
|
y: 0px;
|
||||||
width: root.width;
|
width: parent.width;
|
||||||
height: root.height;
|
height: parent.height;
|
||||||
z: 9;
|
z: 9;
|
||||||
clicked => {
|
clicked => {
|
||||||
root.menu-open = false;
|
root.menu-open = false;
|
||||||
@@ -1185,9 +1396,9 @@ export component MainWindow inherits Window {
|
|||||||
TouchArea { clicked => { focus-sink.focus(); } }
|
TouchArea { clicked => { focus-sink.focus(); } }
|
||||||
DialogSurface {
|
DialogSurface {
|
||||||
x: root.compact-language ? 219px : 190px;
|
x: root.compact-language ? 219px : 190px;
|
||||||
y: 9px;
|
y: 60px;
|
||||||
width: root.compact-language ? 202px : 269px;
|
width: root.compact-language ? 202px : 269px;
|
||||||
height: 432px;
|
height: 330px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -1202,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: 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; }
|
settings-scroll := ScrollView {
|
||||||
CompactComboBox { x: root.compact-language ? 120px : 188px; y: 30px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
|
x: 0px;
|
||||||
Rectangle { x: 7px; y: 57px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
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; }
|
settings-content := VerticalLayout {
|
||||||
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; }
|
// Reserve space for the overlaid vertical scrollbar.
|
||||||
Rectangle { x: 7px; y: 87px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
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; }
|
Rectangle {
|
||||||
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; }
|
height: 30px;
|
||||||
Rectangle { x: 7px; y: 117px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
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; }
|
Rectangle { x: 7px; y: 286px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
||||||
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; }
|
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; } }
|
||||||
Rectangle { x: 7px; y: 147px; width: parent.width - 14px; height: 1px; background: ImGuiLineStyle.separator; }
|
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; } }
|
||||||
|
|
||||||
if root.hardware-codec-visible: Text { x: 8px; y: 151px; width: root.compact-language ? 155px : 225px; height: 24px; text: UiStrings.hardware-codec; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
|
||||||
if root.hardware-codec-visible: ImGuiCheckBox { x: root.compact-language ? 171px : 238px; y: 151px; checked <=> root.hardware-codec-enabled; enabled: !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; } }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1289,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; }
|
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; }
|
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; }
|
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 {
|
Rectangle {
|
||||||
x: root.compact-language ? 100px : 173px;
|
x: root.compact-language ? 100px : 173px;
|
||||||
@@ -1303,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; }
|
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; }
|
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; }
|
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 {
|
Rectangle {
|
||||||
x: root.compact-language ? 100px : 173px;
|
x: root.compact-language ? 100px : 173px;
|
||||||
@@ -1317,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; }
|
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; }
|
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 ? 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; } }
|
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; } }
|
||||||
}
|
}
|
||||||
@@ -1507,4 +1746,19 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.custom-titlebar: Rectangle {
|
||||||
|
x: 0px;
|
||||||
|
y: 0px;
|
||||||
|
width: root.width;
|
||||||
|
height: root.height;
|
||||||
|
background: transparent;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
|
||||||
|
border-radius:
|
||||||
|
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
|
||||||
|
z: 200;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,13 +112,16 @@ component ControlBarButton inherits Rectangle {
|
|||||||
export component StreamWindow inherits Window {
|
export component StreamWindow inherits Window {
|
||||||
title: "CrossDesk";
|
title: "CrossDesk";
|
||||||
preferred-width: 1280px;
|
preferred-width: 1280px;
|
||||||
preferred-height: 720px;
|
preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
|
||||||
min-width: 640px;
|
min-width: 640px;
|
||||||
min-height: 360px;
|
min-height: 360px + (root.custom-titlebar ? 32px : 0px);
|
||||||
no-frame: false;
|
no-frame: root.custom-titlebar;
|
||||||
background: black;
|
background: root.custom-titlebar ? transparent : black;
|
||||||
default-font-size: ImGuiFontStyle.base;
|
default-font-size: ImGuiFontStyle.base;
|
||||||
|
|
||||||
|
in property <bool> custom-titlebar: false;
|
||||||
|
in property <bool> window-active: true;
|
||||||
|
in property <bool> window-maximized: false;
|
||||||
in property <[StreamTab]> tabs;
|
in property <[StreamTab]> tabs;
|
||||||
in-out property <int> selected-tab: 0;
|
in-out property <int> selected-tab: 0;
|
||||||
in property <image> frame;
|
in property <image> frame;
|
||||||
@@ -153,6 +156,10 @@ export component StreamWindow inherits Window {
|
|||||||
callback toggle-stats;
|
callback toggle-stats;
|
||||||
callback toggle-fullscreen;
|
callback toggle-fullscreen;
|
||||||
callback disconnect;
|
callback disconnect;
|
||||||
|
callback stream-title-drag(int, length, length);
|
||||||
|
callback minimize-stream-window;
|
||||||
|
callback toggle-maximize-stream-window;
|
||||||
|
callback close-stream-window;
|
||||||
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
|
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
|
||||||
callback scroll-input(length, length, length, length);
|
callback scroll-input(length, length, length, length);
|
||||||
callback key-input(string, bool, bool, bool, bool, bool);
|
callback key-input(string, bool, bool, bool, bool, bool);
|
||||||
@@ -169,6 +176,9 @@ export component StreamWindow inherits Window {
|
|||||||
private property <length> control-press-x: 0px;
|
private property <length> control-press-x: 0px;
|
||||||
private property <length> control-press-y: 0px;
|
private property <length> control-press-y: 0px;
|
||||||
private property <int> dragging-tab: -1;
|
private property <int> dragging-tab: -1;
|
||||||
|
private property <length> titlebar-height:
|
||||||
|
root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px;
|
||||||
|
private property <length> window-corner-radius: 12px;
|
||||||
callback begin-control-drag(length, length);
|
callback begin-control-drag(length, length);
|
||||||
callback move-control-drag(length, length);
|
callback move-control-drag(length, length);
|
||||||
callback end-control-drag;
|
callback end-control-drag;
|
||||||
@@ -202,10 +212,167 @@ export component StreamWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
window-surface := Rectangle {
|
||||||
width: root.width;
|
width: root.width;
|
||||||
height: root.height;
|
height: root.height;
|
||||||
background: black;
|
background: black;
|
||||||
|
border-radius:
|
||||||
|
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
|
||||||
|
? root.window-corner-radius : 0px;
|
||||||
|
clip: root.custom-titlebar && !root.fullscreen-enabled;
|
||||||
|
|
||||||
|
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
|
||||||
|
x: 0px;
|
||||||
|
y: 0px;
|
||||||
|
width: parent.width;
|
||||||
|
height: root.titlebar-height;
|
||||||
|
background: #f6f5f4;
|
||||||
|
z: 100;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 120px;
|
||||||
|
width: parent.width - 240px;
|
||||||
|
height: parent.height;
|
||||||
|
text: "CrossDesk";
|
||||||
|
color: root.window-active ? #3d3d3d : #747474;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
overflow: elide;
|
||||||
|
}
|
||||||
|
stream-title-drag-area := TouchArea {
|
||||||
|
width: parent.width - 120px;
|
||||||
|
property <int> phase: self.pressed ? 1 : 0;
|
||||||
|
changed phase => {
|
||||||
|
root.stream-title-drag(phase, self.mouse-x, self.mouse-y);
|
||||||
|
}
|
||||||
|
moved => {
|
||||||
|
if (self.pressed) {
|
||||||
|
root.stream-title-drag(2, self.mouse-x, self.mouse-y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double-clicked => {
|
||||||
|
root.toggle-maximize-stream-window();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream-minimize-button := Rectangle {
|
||||||
|
x: parent.width - 114px;
|
||||||
|
y: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: stream-minimize-touch.pressed ? #c0bfbc
|
||||||
|
: stream-minimize-touch.has-hover ? #deddda
|
||||||
|
: root.window-active ? #e6e6e6
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: "Minimize";
|
||||||
|
accessible-action-default => { stream-minimize-touch.clicked(); }
|
||||||
|
Rectangle {
|
||||||
|
x: 8px;
|
||||||
|
y: 13px;
|
||||||
|
width: 8px;
|
||||||
|
height: 0.75px;
|
||||||
|
background: root.window-active
|
||||||
|
|| stream-minimize-touch.has-hover
|
||||||
|
|| stream-minimize-touch.pressed ? #454545 : #898989;
|
||||||
|
}
|
||||||
|
stream-minimize-touch := TouchArea {
|
||||||
|
clicked => { root.minimize-stream-window(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream-maximize-button := Rectangle {
|
||||||
|
x: parent.width - 74px;
|
||||||
|
y: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: stream-maximize-touch.pressed ? #c0bfbc
|
||||||
|
: stream-maximize-touch.has-hover ? #deddda
|
||||||
|
: root.window-active ? #e6e6e6
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: root.window-maximized ? "Restore" : "Maximize";
|
||||||
|
accessible-action-default => { stream-maximize-touch.clicked(); }
|
||||||
|
if !root.window-maximized: Rectangle {
|
||||||
|
x: 8px;
|
||||||
|
y: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: root.window-active
|
||||||
|
|| stream-maximize-touch.has-hover
|
||||||
|
|| stream-maximize-touch.pressed ? #454545 : #898989;
|
||||||
|
}
|
||||||
|
if root.window-maximized: Rectangle {
|
||||||
|
x: 9px;
|
||||||
|
y: 7px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: root.window-active
|
||||||
|
|| stream-maximize-touch.has-hover
|
||||||
|
|| stream-maximize-touch.pressed ? #454545 : #898989;
|
||||||
|
}
|
||||||
|
if root.window-maximized: Rectangle {
|
||||||
|
x: 7px;
|
||||||
|
y: 9px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
background: stream-maximize-button.background;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: root.window-active
|
||||||
|
|| stream-maximize-touch.has-hover
|
||||||
|
|| stream-maximize-touch.pressed ? #454545 : #898989;
|
||||||
|
}
|
||||||
|
stream-maximize-touch := TouchArea {
|
||||||
|
clicked => { root.toggle-maximize-stream-window(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream-close-button := Rectangle {
|
||||||
|
x: parent.width - 34px;
|
||||||
|
y: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: stream-close-touch.pressed ? #b2161d
|
||||||
|
: stream-close-touch.has-hover ? #e01b24
|
||||||
|
: root.window-active ? #e6e6e6
|
||||||
|
: transparent;
|
||||||
|
accessible-role: button;
|
||||||
|
accessible-label: "Close";
|
||||||
|
accessible-action-default => { stream-close-touch.clicked(); }
|
||||||
|
Path {
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height;
|
||||||
|
viewbox-width: 24;
|
||||||
|
viewbox-height: 24;
|
||||||
|
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
|
||||||
|
stroke: stream-close-touch.has-hover
|
||||||
|
|| stream-close-touch.pressed ? white
|
||||||
|
: root.window-active ? #454545 : #898989;
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
stream-close-touch := TouchArea {
|
||||||
|
clicked => { root.close-stream-window(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rectangle {
|
||||||
|
y: parent.height - 1px;
|
||||||
|
height: 1px;
|
||||||
|
background: #d5d3d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content-layer := Rectangle {
|
||||||
|
y: root.titlebar-height;
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height - self.y;
|
||||||
|
background: black;
|
||||||
|
clip: true;
|
||||||
|
|
||||||
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
|
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -341,7 +508,10 @@ export component StreamWindow inherits Window {
|
|||||||
&& (event.kind == PointerEventKind.up
|
&& (event.kind == PointerEventKind.up
|
||||||
|| event.kind == PointerEventKind.cancel) {
|
|| event.kind == PointerEventKind.cancel) {
|
||||||
root.end-control-drag();
|
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);
|
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,5 +815,17 @@ export component StreamWindow inherits Window {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height;
|
||||||
|
background: transparent;
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: #aaa8a6;
|
||||||
|
border-radius:
|
||||||
|
root.window-maximized ? 0px : root.window-corner-radius;
|
||||||
|
z: 200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule submodules/minirtc updated: da4b0f2e9a...e6d796d7b7
@@ -19,7 +19,7 @@ std::filesystem::path FindRepoRoot() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ReadFile(const std::filesystem::path &path) {
|
std::string ReadFile(const std::filesystem::path& path) {
|
||||||
std::ifstream file(path, std::ios::binary);
|
std::ifstream file(path, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {};
|
return {};
|
||||||
@@ -30,8 +30,8 @@ std::string ReadFile(const std::filesystem::path &path) {
|
|||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectContains(const char *name, const std::string &value,
|
bool ExpectContains(const char* name, const std::string& value,
|
||||||
const std::string &expected) {
|
const std::string& expected) {
|
||||||
if (value.find(expected) != std::string::npos) {
|
if (value.find(expected) != std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -40,8 +40,8 @@ bool ExpectContains(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectNotContains(const char *name, const std::string &value,
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
const std::string &unexpected) {
|
const std::string& unexpected) {
|
||||||
if (value.find(unexpected) == std::string::npos) {
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,10 @@ int main() {
|
|||||||
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
|
ReadFile(repo_root / "src/gui/runtime/remote_session.h");
|
||||||
const std::string connection_runtime_cpp =
|
const std::string connection_runtime_cpp =
|
||||||
ReadFile(repo_root / "src/gui/runtime/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;
|
bool ok = true;
|
||||||
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
ok &= ExpectContains("minirtc.h", minirtc_api, "RemoteUnavailable");
|
||||||
@@ -81,6 +85,11 @@ int main() {
|
|||||||
"\"Remote unavailable\"");
|
"\"Remote unavailable\"");
|
||||||
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectContains("peer_connection.cpp", peer_connection,
|
||||||
"ConnectionStatus::RemoteUnavailable");
|
"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,
|
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
||||||
"\"Device offline\"");
|
"\"Device offline\"");
|
||||||
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
ok &= ExpectNotContains("peer_connection.cpp", peer_connection,
|
||||||
@@ -105,6 +114,14 @@ int main() {
|
|||||||
"kConnectionAttemptTimeout");
|
"kConnectionAttemptTimeout");
|
||||||
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
ok &= ExpectContains("connection_runtime.cpp", connection_runtime_cpp,
|
||||||
"connection_attempt_started_at_");
|
"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;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ std::filesystem::path FindRepoRoot() {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ReadFile(const std::filesystem::path &path) {
|
std::string ReadFile(const std::filesystem::path& path) {
|
||||||
std::ifstream file(path, std::ios::binary);
|
std::ifstream file(path, std::ios::binary);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return {};
|
return {};
|
||||||
@@ -28,8 +28,8 @@ std::string ReadFile(const std::filesystem::path &path) {
|
|||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectContains(const char *name, const std::string &value,
|
bool ExpectContains(const char* name, const std::string& value,
|
||||||
const std::string &expected) {
|
const std::string& expected) {
|
||||||
if (value.find(expected) != std::string::npos) {
|
if (value.find(expected) != std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -37,8 +37,8 @@ bool ExpectContains(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectNotContains(const char *name, const std::string &value,
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
const std::string &unexpected) {
|
const std::string& unexpected) {
|
||||||
if (value.find(unexpected) == std::string::npos) {
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -46,8 +46,8 @@ bool ExpectNotContains(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ExpectContainsAtLeast(const char *name, const std::string &value,
|
bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||||
const std::string &expected, size_t min_count) {
|
const std::string& expected, size_t min_count) {
|
||||||
size_t count = 0;
|
size_t count = 0;
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
||||||
@@ -62,7 +62,7 @@ bool ExpectContainsAtLeast(const char *name, const std::string &value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
const std::filesystem::path repo_root = FindRepoRoot();
|
const std::filesystem::path repo_root = FindRepoRoot();
|
||||||
@@ -84,14 +84,16 @@ int main() {
|
|||||||
"root.shortcut-menu-open = false;");
|
"root.shortcut-menu-open = false;");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"if root.display-menu-open: Rectangle");
|
"if root.display-menu-open: Rectangle");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &=
|
||||||
"display-touch.has-hover");
|
ExpectContains("stream_window.slint", stream, "display-touch.has-hover");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"root.switch-display(index)");
|
"root.switch-display(index)");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"if root.shortcut-menu-open: Rectangle");
|
"if root.shortcut-menu-open: Rectangle");
|
||||||
|
ok &=
|
||||||
|
ExpectContains("stream_window.slint", stream, "shortcut-touch.has-hover");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"shortcut-touch.has-hover");
|
"} else if !root.control-dragging && !over-control {");
|
||||||
|
|
||||||
ok &= ExpectContains("common.slint", common,
|
ok &= ExpectContains("common.slint", common,
|
||||||
"if touch.has-hover && root.tooltip != \"\": Rectangle");
|
"if touch.has-hover && root.tooltip != \"\": Rectangle");
|
||||||
@@ -100,32 +102,38 @@ int main() {
|
|||||||
"tooltip: StreamStrings.select-display");
|
"tooltip: StreamStrings.select-display");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"tooltip: StreamStrings.send-shortcut");
|
"tooltip: StreamStrings.send-shortcut");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &=
|
||||||
"root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse");
|
ExpectContains("stream_window.slint", stream,
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
"root.mouse-control-enabled ? StreamStrings.release-mouse "
|
||||||
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
|
": StreamStrings.control-mouse");
|
||||||
|
ok &= ExpectContains(
|
||||||
|
"stream_window.slint", stream,
|
||||||
|
"root.audio-enabled ? StreamStrings.mute : StreamStrings.audio");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"tooltip: StreamStrings.select-file");
|
"tooltip: StreamStrings.select-file");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats");
|
"root.stats-visible ? StreamStrings.hide-stats : "
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
"StreamStrings.show-stats");
|
||||||
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen");
|
ok &=
|
||||||
|
ExpectContains("stream_window.slint", stream,
|
||||||
|
"root.fullscreen-enabled ? StreamStrings.exit-fullscreen "
|
||||||
|
": StreamStrings.fullscreen");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
ok &= ExpectContains("stream_window.slint", stream,
|
||||||
"tooltip: StreamStrings.disconnect");
|
"tooltip: StreamStrings.disconnect");
|
||||||
ok &= ExpectContains("stream_window.slint", stream,
|
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,
|
ok &=
|
||||||
"set_select_display(");
|
ExpectContains("gui_application.cpp", application, "set_select_display(");
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &= ExpectContains("gui_application.cpp", application, "set_show_stats(");
|
||||||
"set_show_stats(");
|
ok &=
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ExpectContains("gui_application.cpp", application, "set_expand_control(");
|
||||||
"set_expand_control(");
|
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &= ExpectContains("gui_application.cpp", application,
|
||||||
"set_collapse_control(");
|
"set_collapse_control(");
|
||||||
ok &= ExpectContains("gui_application.cpp", application,
|
ok &= ExpectContains("gui_application.cpp", application,
|
||||||
"(*ui_->stream)->global<ui::StreamStrings>()");
|
"(*ui_->stream)->global<ui::StreamStrings>()");
|
||||||
ok &= ExpectNotContains("gui_application.cpp", application,
|
ok &=
|
||||||
"#include <imgui");
|
ExpectNotContains("gui_application.cpp", application, "#include <imgui");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,109 @@
|
|||||||
#include "crossdesk_ui.h"
|
#include "crossdesk_ui.h"
|
||||||
|
#include "fa_solid_900.h"
|
||||||
|
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Set the CROSSDESK_*_UI_SNAPSHOT variables while using Slint's software
|
||||||
|
// renderer to export deterministic active and inactive visual-check images.
|
||||||
|
bool WriteWindowSnapshot(const slint::Window &window, const char *path) {
|
||||||
|
const auto snapshot = window.take_snapshot();
|
||||||
|
if (!snapshot) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ofstream output(path, std::ios::binary);
|
||||||
|
output << "P6\n"
|
||||||
|
<< snapshot->width() << ' ' << snapshot->height() << "\n255\n";
|
||||||
|
for (const auto &pixel : *snapshot) {
|
||||||
|
output.put(static_cast<char>(pixel.r));
|
||||||
|
output.put(static_cast<char>(pixel.g));
|
||||||
|
output.put(static_cast<char>(pixel.b));
|
||||||
|
}
|
||||||
|
return output.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RegisterFontAwesome(slint::Window &window) {
|
||||||
|
return !window.window_handle()
|
||||||
|
.register_font_from_data(fa_solid_900_ttf,
|
||||||
|
fa_solid_900_ttf_len)
|
||||||
|
.has_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
auto window = crossdesk::ui::MainWindow::create();
|
auto window = crossdesk::ui::MainWindow::create();
|
||||||
auto stream = crossdesk::ui::StreamWindow::create();
|
auto stream = crossdesk::ui::StreamWindow::create();
|
||||||
auto server = crossdesk::ui::ServerWindow::create();
|
auto server = crossdesk::ui::ServerWindow::create();
|
||||||
|
if (!RegisterFontAwesome(window->window()) ||
|
||||||
|
!RegisterFontAwesome(stream->window()) ||
|
||||||
|
!RegisterFontAwesome(server->window())) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
window->set_local_id("123 456 789");
|
window->set_local_id("123 456 789");
|
||||||
window->set_local_password("123456");
|
window->set_local_password("123456");
|
||||||
window->set_connection_dialog_open(true);
|
window->set_connection_dialog_open(true);
|
||||||
window->set_connection_password_required(true);
|
window->set_connection_password_required(true);
|
||||||
|
window->set_custom_titlebar(true);
|
||||||
|
window->set_wayland_titlebar(true);
|
||||||
window->set_settings_session_active(true);
|
window->set_settings_session_active(true);
|
||||||
|
window->set_hardware_codec_available(false);
|
||||||
window->set_portable_service_settings_visible(true);
|
window->set_portable_service_settings_visible(true);
|
||||||
assert(std::string(window->get_local_id()) == "123 456 789");
|
assert(std::string(window->get_local_id()) == "123 456 789");
|
||||||
assert(window->get_connection_dialog_open());
|
assert(window->get_connection_dialog_open());
|
||||||
assert(window->get_connection_password_required());
|
assert(window->get_connection_password_required());
|
||||||
|
assert(window->get_custom_titlebar());
|
||||||
|
assert(window->get_wayland_titlebar());
|
||||||
assert(window->get_settings_session_active());
|
assert(window->get_settings_session_active());
|
||||||
|
assert(!window->get_hardware_codec_available());
|
||||||
assert(window->get_portable_service_settings_visible());
|
assert(window->get_portable_service_settings_visible());
|
||||||
|
if (const char *snapshot_path =
|
||||||
|
std::getenv("CROSSDESK_MAIN_UI_SNAPSHOT")) {
|
||||||
|
window->show();
|
||||||
|
const bool snapshot_written =
|
||||||
|
WriteWindowSnapshot(window->window(), snapshot_path);
|
||||||
|
window->hide();
|
||||||
|
if (!snapshot_written) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (const char *snapshot_path =
|
||||||
|
std::getenv("CROSSDESK_MAIN_INACTIVE_UI_SNAPSHOT")) {
|
||||||
|
window->set_window_active(false);
|
||||||
|
window->show();
|
||||||
|
const bool snapshot_written =
|
||||||
|
WriteWindowSnapshot(window->window(), snapshot_path);
|
||||||
|
window->hide();
|
||||||
|
window->set_window_active(true);
|
||||||
|
if (!snapshot_written) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
window->set_settings_open(true);
|
window->set_settings_open(true);
|
||||||
window->set_about_open(true);
|
window->set_about_open(true);
|
||||||
assert(window->get_settings_open());
|
assert(window->get_settings_open());
|
||||||
assert(window->get_about_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->set_remote_id_input("987654321");
|
||||||
window->invoke_reset_remote_id();
|
window->invoke_reset_remote_id();
|
||||||
assert(std::string(window->get_remote_id_input()).empty());
|
assert(std::string(window->get_remote_id_input()).empty());
|
||||||
@@ -62,7 +142,37 @@ int main() {
|
|||||||
stream->set_tabs(
|
stream->set_tabs(
|
||||||
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
|
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
|
||||||
std::vector{tab}));
|
std::vector{tab}));
|
||||||
|
stream->set_custom_titlebar(true);
|
||||||
|
stream->set_window_maximized(true);
|
||||||
assert(stream->get_tabs()->row_count() == 1);
|
assert(stream->get_tabs()->row_count() == 1);
|
||||||
|
assert(stream->get_custom_titlebar());
|
||||||
|
assert(stream->get_window_maximized());
|
||||||
|
if (const char *snapshot_path =
|
||||||
|
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
|
||||||
|
stream->set_window_maximized(false);
|
||||||
|
stream->show();
|
||||||
|
const bool snapshot_written =
|
||||||
|
WriteWindowSnapshot(stream->window(), snapshot_path);
|
||||||
|
stream->hide();
|
||||||
|
stream->set_window_maximized(true);
|
||||||
|
if (!snapshot_written) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (const char *snapshot_path =
|
||||||
|
std::getenv("CROSSDESK_STREAM_INACTIVE_UI_SNAPSHOT")) {
|
||||||
|
stream->set_window_maximized(false);
|
||||||
|
stream->set_window_active(false);
|
||||||
|
stream->show();
|
||||||
|
const bool snapshot_written =
|
||||||
|
WriteWindowSnapshot(stream->window(), snapshot_path);
|
||||||
|
stream->hide();
|
||||||
|
stream->set_window_active(true);
|
||||||
|
stream->set_window_maximized(true);
|
||||||
|
if (!snapshot_written) {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool tab_reordered = false;
|
bool tab_reordered = false;
|
||||||
stream->on_reorder_tab([&](int index, float x, float width) {
|
stream->on_reorder_tab([&](int index, float x, float width) {
|
||||||
@@ -71,6 +181,12 @@ int main() {
|
|||||||
stream->invoke_reorder_tab(0, 120.0f, 110.0f);
|
stream->invoke_reorder_tab(0, 120.0f, 110.0f);
|
||||||
assert(tab_reordered);
|
assert(tab_reordered);
|
||||||
|
|
||||||
|
bool maximize_requested = false;
|
||||||
|
stream->on_toggle_maximize_stream_window(
|
||||||
|
[&] { maximize_requested = true; });
|
||||||
|
stream->invoke_toggle_maximize_stream_window();
|
||||||
|
assert(maximize_requested);
|
||||||
|
|
||||||
crossdesk::ui::FileTransferEntry transfer;
|
crossdesk::ui::FileTransferEntry transfer;
|
||||||
transfer.name = "archive.zip";
|
transfer.name = "archive.zip";
|
||||||
transfer.status = "Sending";
|
transfer.status = "Sending";
|
||||||
|
|||||||
Vendored
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package("rust")
|
||||||
|
set_kind("toolchain")
|
||||||
|
set_homepage("https://rust-lang.org")
|
||||||
|
set_description("Rust is a general-purpose programming language emphasizing performance, type safety, and concurrency.")
|
||||||
|
|
||||||
|
add_versions("1.92.0", "")
|
||||||
|
|
||||||
|
add_deps("ca-certificates", {host = true, private = true})
|
||||||
|
add_deps("rustup", {host = true, private = true, system = false})
|
||||||
|
|
||||||
|
on_install(function(package)
|
||||||
|
import("core.tools.rustc.target_triple")
|
||||||
|
|
||||||
|
local host_target = assert(target_triple(package:plat(), package:arch()),
|
||||||
|
"failed to determine the Rust host target triple")
|
||||||
|
local version = package:version():shortstr()
|
||||||
|
local toolchain_name = version .. "-" .. host_target
|
||||||
|
local rustup_home = assert(os.getenv("RUSTUP_HOME"), "cannot find rustup home")
|
||||||
|
|
||||||
|
os.vrunv("rustup", {"install", "--no-self-update", toolchain_name})
|
||||||
|
os.vmv(path.join(rustup_home, "toolchains", toolchain_name, "*"), package:installdir())
|
||||||
|
|
||||||
|
-- Keep the Xmake-managed toolchain isolated from rustup after installation.
|
||||||
|
os.vrm(path.join(rustup_home, "toolchains", toolchain_name))
|
||||||
|
os.vrm(path.join(rustup_home, "update-hashes", toolchain_name))
|
||||||
|
|
||||||
|
package:addenv("RC", "bin/rustc" .. (is_host("windows") and ".exe" or ""))
|
||||||
|
package:mark_as_pathenv("RC")
|
||||||
|
end)
|
||||||
|
|
||||||
|
on_test(function(package)
|
||||||
|
os.vrun("cargo --version")
|
||||||
|
os.vrun("rustc --version")
|
||||||
|
end)
|
||||||
|
package_end()
|
||||||
Vendored
+1
@@ -10,6 +10,7 @@ package("slint")
|
|||||||
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
|
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
|
||||||
|
|
||||||
add_deps("cmake")
|
add_deps("cmake")
|
||||||
|
add_deps("rust 1.92.0", {host = true, private = true, system = false})
|
||||||
|
|
||||||
on_load(function(package)
|
on_load(function(package)
|
||||||
package:add("includedirs", "include/slint")
|
package:add("includedirs", "include/slint")
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
includes("cpp-httplib", "slint")
|
includes("cpp-httplib", "rust", "slint")
|
||||||
|
|||||||
@@ -3,6 +3,22 @@ function setup_targets()
|
|||||||
|
|
||||||
includes("submodules", "thirdparty")
|
includes("submodules", "thirdparty")
|
||||||
|
|
||||||
|
local function copy_slint_runtime(target)
|
||||||
|
if not target:is_plat("windows") then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local slint = target:pkg("slint")
|
||||||
|
if not slint and target:dep("gui") then
|
||||||
|
slint = target:dep("gui"):pkg("slint")
|
||||||
|
end
|
||||||
|
assert(slint, "the Slint package is required to copy its Windows runtime")
|
||||||
|
local runtime_dir = path.join(slint:installdir(), "lib")
|
||||||
|
local runtime_dll = path.join(runtime_dir, "slint_cpp.dll")
|
||||||
|
assert(os.isfile(runtime_dll), "Slint runtime not found: " .. runtime_dll)
|
||||||
|
os.cp(runtime_dll, target:targetdir())
|
||||||
|
end
|
||||||
|
|
||||||
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
|
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
|
||||||
if is_config("CROSSDESK_PORTABLE", true) then
|
if is_config("CROSSDESK_PORTABLE", true) then
|
||||||
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
|
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
|
||||||
@@ -87,10 +103,15 @@ function setup_targets()
|
|||||||
set_kind("binary")
|
set_kind("binary")
|
||||||
set_languages("c++20")
|
set_languages("c++20")
|
||||||
set_default(false)
|
set_default(false)
|
||||||
|
if is_os("windows") then
|
||||||
|
add_cxxflags("/bigobj")
|
||||||
|
end
|
||||||
add_packages("slint")
|
add_packages("slint")
|
||||||
|
add_includedirs("src/gui/assets/fonts")
|
||||||
add_rules("slint")
|
add_rules("slint")
|
||||||
add_files("src/gui/ui/crossdesk_ui.slint")
|
add_files("src/gui/ui/crossdesk_ui.slint")
|
||||||
add_files("tests/slint_ui_smoke_test.cpp")
|
add_files("tests/slint_ui_smoke_test.cpp")
|
||||||
|
after_build(copy_slint_runtime)
|
||||||
|
|
||||||
target("version_checker_test")
|
target("version_checker_test")
|
||||||
set_kind("binary")
|
set_kind("binary")
|
||||||
@@ -296,4 +317,5 @@ function setup_targets()
|
|||||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||||
add_files(crossdesk_windows_resource)
|
add_files(crossdesk_windows_resource)
|
||||||
end
|
end
|
||||||
|
after_build(copy_slint_runtime)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user