[feat] add HarmonyOS desktop and mobile clients,issue #117

This commit is contained in:
kunkundi
2026-10-16 00:42:55 +08:00
parent 43a0789c1e
commit 45bf6ec4ee
149 changed files with 11587 additions and 93 deletions
@@ -53,7 +53,12 @@
#elif defined(__APPLE__)
#include "platform/macos/gui/tray/mac_tray.h"
#include "platform/window_drag.h"
#elif defined(__linux__)
#elif defined(__OHOS__)
#include "platform/harmony/harmony_autostart.h"
#include "platform/harmony/gui/tray/harmony_tray.h"
#include "platform/harmony/gui/window_state.h"
#include "tools/file_transfer.h"
#elif (defined(__linux__) && !defined(__OHOS__))
#include <GL/gl.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
@@ -70,6 +75,7 @@
#include "signal_server_settings.h"
#include "signal_server_address.h"
#include "ui/announcement_text.h"
#include "ui/model_update.h"
#include "ui/release_notes_text.h"
#include "ui/ui_localization.h"
#include "update_checker.h"
@@ -111,7 +117,7 @@ std::string CreatePasswordChangeRequestId(uint64_t sequence) {
std::to_string(sequence);
}
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
bool HasNonEmptyEnvironmentVariable(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] != '\0';
@@ -639,14 +645,14 @@ struct WindowDragState {
float global_to_physical_scale = 1;
slint::PhysicalPosition window_start;
slint::PhysicalPosition last_target;
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
std::unique_ptr<Display, decltype(&XCloseDisplay)> x11_display{nullptr,
XCloseDisplay};
slint::Timer pointer_timer;
#endif
bool ReadGlobalPointer(float* x, float* y, bool* left_pressed = nullptr) {
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
const char* driver = SDL_GetCurrentVideoDriver();
if (driver && std::strcmp(driver, "x11") == 0) {
// Slint can dispatch motion before SDL pumps its XInput2 events, and
@@ -683,7 +689,7 @@ bool CanUseGlobalPointerPosition() {
if (driver == nullptr) {
return false;
}
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
// Wayland deliberately does not expose global pointer coordinates or allow
// clients to position top-level windows.
return std::strcmp(driver, "wayland") != 0;
@@ -821,7 +827,7 @@ void DragWindow(WindowHandle& component, int phase, float mouse_x,
state.global_to_physical_scale = 1.0f;
#endif
}
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (state.use_global_pointer && state.x11_display) {
// Moving the window can leave consecutive pointer events at identical
// local coordinates, so Slint need not emit another TouchArea::moved.
@@ -841,7 +847,7 @@ void DragWindow(WindowHandle& component, int phase, float mouse_x,
return;
}
if (phase == 0) {
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
state.pointer_timer.stop();
if (state.use_global_pointer && state.x11_display) {
DragWindow(component, 2, mouse_x, mouse_y, state);
@@ -1017,14 +1023,16 @@ struct GuiApplication::SlintUi {
std::unique_ptr<WinTray> tray;
#elif defined(__APPLE__)
std::unique_ptr<MacTray> tray;
#elif defined(__linux__)
#elif defined(__OHOS__)
std::unique_ptr<HarmonyTray> tray;
#elif (defined(__linux__) && !defined(__OHOS__))
std::unique_ptr<LinuxTray> tray;
Display* x11_focus_display = nullptr;
std::chrono::steady_clock::time_point last_x11_activation_poll{};
#endif
~SlintUi() {
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (x11_focus_display) {
XCloseDisplay(x11_focus_display);
}
@@ -1059,7 +1067,7 @@ int GuiApplication::Run() {
}
InitializeSettings();
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().BindSettings(
config_center_.get(), [this] {
std::lock_guard<std::mutex> lock(password_change_mutex_);
@@ -1121,7 +1129,7 @@ void GuiApplication::InitializeSettings() {
}
bool GuiApplication::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
ConfigureLinuxCudaGraphics(config_center_->IsHardwareVideoCodec());
// Standard Wayland top-level windows cannot choose an absolute screen
// position. Use the session's XWayland compatibility server for the GUI so
@@ -1146,7 +1154,7 @@ bool GuiApplication::InitializeSDL() {
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
return false;
}
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
const char* active_video_driver = SDL_GetCurrentVideoDriver();
// X11 permits the manual move/position operations used by the frameless
// main window. Keep native Wayland decorations as a compatibility fallback.
@@ -1192,7 +1200,7 @@ void GuiApplication::InitializeUi() {
LOG_INFO("Using requested Slint backend: {}", requested_slint_backend);
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
// Detect the desktop session before temporarily hiding Wayland variables
// for the XWayland GUI backend below.
const bool is_wayland_session = IsWaylandSession();
@@ -1214,7 +1222,7 @@ void GuiApplication::InitializeUi() {
std::make_unique<SlintVideoPresenter>(*video_renderer_);
#if _WIN32
ui_->main->set_custom_titlebar(true);
#elif defined(__linux__)
#elif (defined(__linux__) && !defined(__OHOS__))
ui_->main->set_custom_titlebar(use_x11_custom_titlebar_);
ui_->main->set_wayland_titlebar(use_xwayland_gui_);
// Set this only at startup so dismissing the notice lasts until exit.
@@ -1342,7 +1350,11 @@ void GuiApplication::InitializeSystemTray() {
ui_->tray = std::make_unique<MacTray>(
std::move(show_window), std::move(hide_window), std::move(open_settings),
std::move(exit_app), "CrossDesk", localization_language_index_);
#elif defined(__linux__)
#elif defined(__OHOS__)
ui_->tray = std::make_unique<HarmonyTray>(
std::move(show_window), std::move(hide_window), std::move(open_settings),
std::move(exit_app), "CrossDesk", localization_language_index_);
#elif (defined(__linux__) && !defined(__OHOS__))
ui_->tray = std::make_unique<LinuxTray>(
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), "CrossDesk",
@@ -1384,6 +1396,9 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
main->set_force_relay_enabled(config_center_->IsForceRelay());
main->set_self_hosted_enabled(enable_self_hosted_);
main->set_autostart_enabled(enable_autostart_);
#if defined(__OHOS__)
main->set_autostart_system_managed(true);
#endif
main->set_daemon_enabled(enable_daemon_);
main->set_save_remote_previews(config_center_->IsSaveRemotePreviews());
main->set_privacy_on_connect_enabled(
@@ -1393,12 +1408,20 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
main->set_capture_method_index(
static_cast<int>(config_center_->GetScreenCaptureMethod()));
#endif
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
main->set_privacy_setting_available(true);
#else
main->set_privacy_setting_available(false);
#endif
#if defined(__OHOS__)
// The sandbox default is not the system desktop. Show the actual receive
// location until a public directory has been selected and authorized.
main->set_file_save_path(UiText(file_transfer_save_path_buf_[0]
? std::string(file_transfer_save_path_buf_)
: FileReceiver().OutputDir().string()));
#else
main->set_file_save_path(file_transfer_save_path_buf_);
#endif
main->set_server_host(signal_server_ip_self_);
main->set_server_port(signal_server_port_self_);
main->set_self_host_settings_error("");
@@ -1633,6 +1656,9 @@ void GuiApplication::BindMainCallbacks(bool gallery_window) {
main->on_cancel_settings([this, main] {
ResetSettingsUi(main->get_gallery_mode());
});
#if defined(__OHOS__)
main->on_refresh_autostart_status([] { RefreshHarmonyAutostartStatus(); });
#endif
main->on_save_self_hosted_settings([this, main] {
// Stage the endpoint until the surrounding settings dialog is confirmed.
return ValidateSelfHostedSettingsFromUi(main->get_gallery_mode());
@@ -1832,12 +1858,19 @@ void GuiApplication::BindStreamCallbacks() {
});
stream->on_send_shortcut([this](slint::SharedString shortcut) {
auto props = SelectedSession();
if (!props || !props->peer_) {
if (!props || !props->peer_ || !props->control_mouse_ ||
props->connection_status_.load() != ConnectionStatus::Connected) {
return;
}
const std::string name(shortcut);
if (name != "Ctrl+Alt+Del" && name != "Win+L") return;
{
std::lock_guard lock(props->remote_version_mutex_);
if (name == "Ctrl+Alt+Del" && props->remote_platform_ == HostPlatform::HarmonyOS) return;
}
RemoteAction action{};
action.type = ControlType::service_command;
action.c.flag = std::string(shortcut) == "Ctrl+Alt+Del"
action.c.flag = name == "Ctrl+Alt+Del"
? ServiceCommandFlag::send_sas
: ServiceCommandFlag::lock_workstation;
const std::string message = action.to_json();
@@ -2099,7 +2132,7 @@ void GuiApplication::Tick() {
signal_connection_indicator_.Reset();
CreateConnectionPeer();
}
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().Poll(
signal_connected_.load(),
[this](const std::string& password) { RequestPasswordChange(password); },
@@ -2130,7 +2163,7 @@ void GuiApplication::Tick() {
}
}
clipboard_.ApplyPendingRemoteText();
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (ui_->tray) {
ui_->tray->ProcessEvents();
}
@@ -2156,7 +2189,7 @@ void GuiApplication::Tick() {
HandlePresenceProbeTimeout();
HandlePrivacy();
HandleWindowsServiceIntegration();
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#else
SyncStreamKeyboardFocus();
@@ -2367,7 +2400,7 @@ void GuiApplication::ShareLocalCursorState() {
bool GuiApplication::RequestPasswordChange(const std::string &password) {
const auto notify = [](const std::string& message) {
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().NotifyKey(message.c_str());
#else
(void)message;
@@ -2486,7 +2519,7 @@ void GuiApplication::HandlePasswordChangeResult() {
}
if (!succeeded) {
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().NotifyKey(
uncertain ? "console_password_uncertain" : "console_password_rejected");
#endif
@@ -2517,7 +2550,7 @@ void GuiApplication::HandlePasswordChangeResult() {
}
LOG_INFO("Password changed successfully for [{}]", client_id_);
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().NotifyKey("console_password_confirmed");
#endif
if (peer_) {
@@ -2551,7 +2584,7 @@ void GuiApplication::HandleCredentialRecovery() {
}
if (promote_pending) {
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().NotifyKey("console_password_recovered");
#endif
if (!settings_.PromotePendingPasswordChange()) {
@@ -2560,7 +2593,7 @@ void GuiApplication::HandleCredentialRecovery() {
LOG_INFO("Recovered password change with pending credential");
}
} else if (clear_pending) {
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().NotifyKey("console_password_retained");
#endif
if (!settings_.ClearPendingPasswordChange()) {
@@ -2699,6 +2732,17 @@ void GuiApplication::SyncMainWindow() {
const bool signal_online = signal_connected_.load();
const bool signal_reconnecting = signal_connection_indicator_.Update(signal_online);
for (const auto& main : {ui_->main, ui_->wall}) {
#if defined(__OHOS__)
// Only the operating system owns this setting. Never bind it to the local
// saved checkbox or wait for an ArkTS request in this 16 ms GUI tick.
switch (GetHarmonyAutostartStatus()) {
case HarmonyAutostartStatus::checking: main->set_autostart_status(ui::AutostartStatus::Checking); break;
case HarmonyAutostartStatus::enabled: main->set_autostart_status(ui::AutostartStatus::Enabled); break;
case HarmonyAutostartStatus::disabled: main->set_autostart_status(ui::AutostartStatus::Disabled); break;
case HarmonyAutostartStatus::unsupported: main->set_autostart_status(ui::AutostartStatus::Unsupported); break;
case HarmonyAutostartStatus::failed: main->set_autostart_status(ui::AutostartStatus::Failed); break;
}
#endif
main->set_signal_connected(signal_online);
main->set_signal_reconnecting(signal_reconnecting);
main->set_signal_tls_error(signal_status_ ==
@@ -2983,7 +3027,7 @@ void GuiApplication::SyncPlatformDialogs() {
#endif
}
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
void GuiApplication::SyncXWaylandWindowActivation() {
if (!ui_ || !use_xwayland_gui_) {
return;
@@ -3057,12 +3101,20 @@ void GuiApplication::SetStreamKeyboardFocus(bool focused) {
}
void GuiApplication::SyncStreamKeyboardFocus() {
#if defined(__OHOS__)
if (ui_) {
ui_->main->set_window_active(IsHarmonyWindowActive(ui_->main->window()));
ui_->wall->set_window_active(IsHarmonyWindowActive(ui_->wall->window()));
}
#endif
if (ui_ && !ui_->gallery_control_id.empty()) {
#if defined(__APPLE__)
if (!IsGalleryWindowActive()) ReleaseControl();
#elif defined(_WIN32)
if (GetForegroundWindow() != ui_->wall->window().win32_hwnd())
ReleaseControl();
#elif defined(__OHOS__)
if (!IsHarmonyWindowActive(ui_->wall->window())) ReleaseControl();
#endif
return;
}
@@ -3077,6 +3129,10 @@ void GuiApplication::SyncStreamKeyboardFocus() {
GetForegroundWindow() == stream_hwnd);
#elif defined(__APPLE__)
SetStreamKeyboardFocus(IsStreamWindowActive());
#elif defined(__OHOS__)
const bool active = IsHarmonyWindowActive((*ui_->stream)->window());
(*ui_->stream)->set_window_active(active);
SetStreamKeyboardFocus(active);
#endif
}
@@ -3093,7 +3149,7 @@ void GuiApplication::SyncStreamWindow() {
if (!has_sessions) need_to_create_stream_window_ = false;
if (need_to_create_stream_window_ && !ui_->stream) {
ui_->stream.emplace(ui::StreamWindow::create());
#if defined(__linux__)
#if (defined(__linux__) && !defined(__OHOS__))
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
#endif
RegisterFontAwesome((*ui_->stream)->window());
@@ -3257,6 +3313,9 @@ void GuiApplication::SyncStreamWindow() {
}
}
(*ui_->stream)->set_remote_update_visible(remote_update_available);
// All desktop clients use the same menu for a Harmony host. Windows SAS is
// not an ordinary key chord and has no public Harmony implementation.
(*ui_->stream)->set_secure_attention_supported(remote_platform != HostPlatform::HarmonyOS);
if (remote_update_available) {
SetStreamKeyboardFocus(false);
}
@@ -3358,7 +3417,7 @@ void GuiApplication::SyncStreamWindow() {
index, props->display_info_list_[index].name,
localization_language_index_)));
}
ui_->display_model->set_vector(std::move(displays));
ui::UpdateModelRows(*ui_->display_model, std::move(displays));
(*ui_->stream)->set_selected_display(props->selected_display_);
const auto net = props->net_traffic_stats_.Load();
@@ -3383,7 +3442,7 @@ void GuiApplication::SyncStreamWindow() {
net.data_inbound_stats, net.data_outbound_stats);
append_stats_row(localization::total[localization_language_index_],
net.total_inbound_stats, net.total_outbound_stats);
ui_->stats_model->set_vector(std::move(stats_rows));
ui::UpdateModelRows(*ui_->stats_model, std::move(stats_rows));
(*ui_->stream)
->set_stats_resolution(UiText(std::to_string(props->video_width_) + "x" +
std::to_string(props->video_height_)));
@@ -3444,7 +3503,7 @@ void GuiApplication::SyncStreamWindow() {
item.size = UiText(FormatFileSize(info.file_size));
transfers.push_back(std::move(item));
}
ui_->transfer_model->set_vector(std::move(transfers));
ui::UpdateModelRows(*ui_->transfer_model, std::move(transfers));
(*ui_->stream)
->set_file_transfer_visible(
props->file_transfer_.file_transfer_window_visible_);
@@ -3698,7 +3757,7 @@ void GuiApplication::SyncServerWindow() {
// Keep the panel alive throughout privacy transitions so remote
// controllers can still see and operate it without resetting its UI state.
bool show_controller_window = has_connected_controller;
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__)
#if !defined(_WIN32) && !defined(__APPLE__) && !(defined(__linux__) && !defined(__OHOS__))
// Preserve the existing panel behavior on other platforms.
const auto privacy_status = privacy_.Snapshot();
const bool privacy_covering = privacy_status.overlay_active ||
@@ -3902,7 +3961,9 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
main->get_hardware_codec_enabled();
enable_turn_ = main->get_turn_enabled();
enable_self_hosted_ = main->get_self_hosted_enabled();
#if !defined(__OHOS__)
enable_autostart_ = main->get_autostart_enabled();
#endif
enable_daemon_ = main->get_daemon_enabled();
localization_language_ =
@@ -3917,7 +3978,9 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
config_center_->SetHardwareVideoCodec(enable_hardware_video_codec_);
config_center_->SetTurn(enable_turn_, main->get_force_relay_enabled());
main->set_force_relay_enabled(config_center_->IsForceRelay());
#if !defined(__OHOS__)
config_center_->SetAutostart(enable_autostart_);
#endif
config_center_->SetDaemon(enable_daemon_);
if (config_center_->SetSaveRemotePreviews(main->get_save_remote_previews()) != 0) {
LOG_ERROR("Failed to save remote preview preference");
@@ -3945,7 +4008,7 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
main->set_capture_method_index(
static_cast<int>(config_center_->GetScreenCaptureMethod()));
#endif
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
if (config_center_->SetPrivacyScreen(main->get_privacy_on_connect_enabled()) !=
0) {
main->set_privacy_on_connect_enabled(
@@ -4000,7 +4063,7 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
enable_autostart_last_ = enable_autostart_;
enable_daemon_last_ = enable_daemon_;
file_transfer_save_path_last_ = path;
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().SetLanguage(localization_language_index_);
#endif
if (ui_) {
@@ -4449,7 +4512,7 @@ void GuiApplication::ShowGalleryWindow() {
#elif defined(_WIN32)
ConfigureWindowsWindowIcons(window.win32_hwnd());
SetForegroundWindow(window.win32_hwnd());
#else
#elif defined(__linux__) && !defined(__OHOS__)
if (!ui_->x11_focus_display) ui_->x11_focus_display = XOpenDisplay(nullptr);
X11ActivateGalleryWindow(ui_->x11_focus_display);
#endif
@@ -5758,7 +5821,7 @@ void GuiApplication::SyncGallery() {
}
void GuiApplication::Cleanup() {
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().BindSettings(nullptr, {}, {});
#endif
if (!ui_) {
@@ -91,7 +91,7 @@ private:
void SyncServerWindow();
void SetServerPanelExpanded(bool expanded);
void PositionServerWindows();
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization();
@@ -127,7 +127,7 @@ private:
uint32_t cursor_state_sequence_ = 0;
std::chrono::steady_clock::time_point next_video_frame_time_{};
SignalConnectionIndicator signal_connection_indicator_;
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
bool use_x11_custom_titlebar_ = false;
#endif
@@ -30,7 +30,7 @@ namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
#ifdef _WIN32
#if defined(_WIN32) || defined(__OHOS__)
constexpr auto kKeyboardCaptureRetryDelay = std::chrono::seconds(1);
#endif
@@ -419,7 +419,7 @@ int SessionDeviceManager::StartMouseController() {
return -1;
}
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (IsWaylandSession()) {
if (!screen_capturer_) {
return 1;
@@ -467,7 +467,7 @@ int SessionDeviceManager::StopMouseController() {
int SessionDeviceManager::StartKeyboardCapturer() {
captured_keyboard_inputs_.Clear();
owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef _WIN32
#if defined(_WIN32) || defined(__OHOS__)
next_keyboard_capture_retry_ =
std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay;
#endif
@@ -479,7 +479,7 @@ int SessionDeviceManager::StartKeyboardCapturer() {
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (IsWaylandSession()) {
owner_.keyboard_capturer_uses_window_events_ = true;
LOG_INFO("Start keyboard capturer with Slint Wayland backend");
@@ -546,7 +546,7 @@ int SessionDeviceManager::InitializeAudioOutput() {
};
if (!open_device()) {
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
LOG_WARN("Failed to open output stream with driver [{}]: {}",
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER")
: "(default)",
@@ -629,7 +629,7 @@ void SessionDeviceManager::UpdateInteractions() {
}
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
const bool is_wayland_session = IsWaylandSession();
const bool stop_wayland_mouse_before_screen =
is_wayland_session && !owner_.start_screen_capturer_ &&
@@ -664,24 +664,27 @@ void SessionDeviceManager::UpdateInteractions() {
}
if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) {
#ifdef _WIN32
#if defined(_WIN32) || defined(__OHOS__)
const int active_display = screen_capturer_->GetCurrentMonitorIndex();
const auto displays = display_info_list();
if (active_display >= 0 &&
active_display < static_cast<int>(displays.size())) {
owner_.selected_display_ = active_display;
#ifdef _WIN32
if (displays[active_display].width <= 0 ||
displays[active_display].height <= 0)
ReleaseRemoteMouseButtons();
#endif
}
#endif
}
if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_ &&
!owner_.privacy_.Snapshot().input_blocked) {
#ifdef _WIN32
#if defined(_WIN32) || defined(__OHOS__)
if (owner_.keyboard_capturer_is_started_ &&
owner_.keyboard_capturer_uses_window_events_ && keyboard_capturer_ &&
keyboard_capturer_->ShouldRetryHook() &&
std::chrono::steady_clock::now() >= next_keyboard_capture_retry_) {
// A transient installation failure or stalled hook must not leave
// system shortcuts in the window-event fallback until focus changes.
@@ -699,7 +702,7 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.keyboard_.ForceReleasePressedKeys();
owner_.keyboard_capturer_uses_window_events_ = true;
owner_.keyboard_capturer_is_started_ = true;
#ifdef _WIN32
#if defined(_WIN32) || defined(__OHOS__)
next_keyboard_capture_retry_ =
std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay;
#endif
@@ -111,8 +111,10 @@ private:
std::vector<DisplayInfo> display_info_list_;
size_t registered_display_stream_count_ = 0;
CapturedKeyboardQueue captured_keyboard_inputs_;
#ifdef _WIN32
#if defined(_WIN32) || defined(__OHOS__)
std::chrono::steady_clock::time_point next_keyboard_capture_retry_{};
#endif
#ifdef _WIN32
std::mutex service_mouse_mutex_;
unsigned service_mouse_buttons_ = 0;
int service_mouse_x_ = 0, service_mouse_y_ = 0;
@@ -16,7 +16,7 @@
#if defined(__APPLE__)
#include "platform/macos/gui/metal_video_renderer.h"
#else
#elif !defined(__OHOS__)
#if defined(_WIN32)
#include <windows.h>
#endif
@@ -28,10 +28,10 @@
namespace crossdesk {
namespace {
#if !defined(__APPLE__)
#if !defined(__APPLE__) && !defined(__OHOS__)
constexpr GLint kGlClampToEdge = 0x812F;
constexpr float kStreamWindowCornerRadius = 12.0f;
#else
#elif defined(__APPLE__)
constexpr int kMetalAttachmentAttemptLimit = 30;
#endif
@@ -78,7 +78,7 @@ struct SlintVideoPresenter::Impl {
#if defined(__APPLE__)
int attachment_attempts = 0;
bool skip_attachment_once = false;
#else
#elif !defined(__OHOS__)
std::mutex gl_mutex;
std::vector<uint8_t> gl_conversion_frame;
std::vector<uint8_t> gl_pending_frame;
@@ -109,7 +109,7 @@ struct SlintVideoPresenter::Impl {
return true;
}
#if !defined(__APPLE__)
#if !defined(__APPLE__) && !defined(__OHOS__)
void ConfigureOpenGlNotifier() {
if (!stream) {
return;
@@ -238,7 +238,7 @@ void SlintVideoPresenter::PrepareWindow(
stream->set_native_video_enabled(false);
impl_->attachment_attempts = kMetalAttachmentAttemptLimit;
impl_->skip_attachment_once = true;
#else
#elif !defined(__OHOS__)
impl_->ConfigureOpenGlNotifier();
#endif
}
@@ -351,7 +351,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
}
return result;
}
#else
#elif !defined(__OHOS__)
auto* opengl_renderer =
dynamic_cast<OpenGlVideoRenderer*>(&impl_->renderer);
if (opengl_renderer && opengl_renderer->IsReady()) {
@@ -419,7 +419,7 @@ SlintVideoPresenter::PresentResult SlintVideoPresenter::Present(
}
}
#if !defined(__APPLE__)
#if !defined(__APPLE__) && !defined(__OHOS__)
uint32_t texture_id = 0;
{
std::lock_guard lock(impl_->gl_mutex);
@@ -229,7 +229,7 @@ void GuiRuntime::HandleServerControllerDisconnected(
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 (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (IsWaylandSession()) {
// Keep Wayland capture session warm to avoid black screen on subsequent
// reconnects.
@@ -63,7 +63,7 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
} // namespace crossdesk
#elif defined(__linux__) && !defined(__APPLE__)
#elif (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
#include <X11/Xlib.h>
#include <X11/extensions/Xfixes.h>
+3 -3
View File
@@ -16,7 +16,7 @@
#include "localization.h"
#include "platform/video_renderer.h"
#include "rd_log.h"
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
#include "platform/linux/headless/headless_console.h"
#endif
@@ -143,7 +143,7 @@ int GuiRuntime::CreateConnectionPeer() {
const char *active_identity =
self_hosted ? self_hosted_user_id_ : client_id_with_password_;
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
// Publish the committed identity; an unconfirmed recovery credential must
// not be advertised as the current connection password.
HeadlessConsole::Instance().SetIdentity(active_identity);
@@ -177,7 +177,7 @@ int GuiRuntime::CreateConnectionPeer() {
sizeof(params_.log_path) - 1);
params_.log_path[sizeof(params_.log_path) - 1] = '\0';
params_.hardware_acceleration = config_center_->IsHardwareVideoCodec();
#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__)
#if defined(_WIN32) || (defined(__linux__) && !defined(__OHOS__)) || defined(__APPLE__)
// Windows and Linux retain pooled CPU NV12 frames or CUDA device frames;
// macOS retains VideoToolbox CVPixelBuffers for direct Metal sampling. All
// platforms fall back to a packed CPU copy when native upload is unavailable.
@@ -24,6 +24,9 @@
#include "platform.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#ifdef __OHOS__
#include "platform/harmony/input/system_shortcuts.h"
#endif
#if _WIN32
#include "desktop_transition_policy.h"
@@ -308,6 +311,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
GetLastError());
}
}
#elif defined(__OHOS__)
if (!SendHarmonySystemShortcut(remote_action.c.flag))
LOG_WARN("Harmony system shortcut unsupported or unauthorized, command={}",
static_cast<int>(remote_action.c.flag));
#endif
return;
}
@@ -22,7 +22,7 @@
#include "platform/video_renderer.h"
#include "rd_log.h"
#include "runtime/gui_runtime.h"
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
#include "platform/linux/headless/headless_console.h"
#endif
@@ -38,7 +38,9 @@ namespace {
constexpr HostPlatform kClientPlatform = HostPlatform::Windows;
#elif defined(__APPLE__)
constexpr HostPlatform kClientPlatform = HostPlatform::MacOS;
#elif defined(__linux__)
#elif defined(__OHOS__)
constexpr HostPlatform kClientPlatform = HostPlatform::HarmonyOS;
#elif (defined(__linux__) && !defined(__OHOS__))
constexpr HostPlatform kClientPlatform = HostPlatform::Linux;
#else
constexpr HostPlatform kClientPlatform = HostPlatform::Unknown;
@@ -558,7 +560,7 @@ void PeerEventHandler::OnNetStatusReport(
LOG_INFO("Use client id [{}] and save id into cache file", id);
runtime->settings_.Save();
}
#ifdef __linux__
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().SetIdentity(id + "@" + password);
#endif
}
+23 -4
View File
@@ -182,6 +182,17 @@ export component CompactComboBox inherits Rectangle {
property <float> popup-progress: 0.0;
callback selected(int);
// Session state can disable a setting while its popup is already open.
// Remove the stale options and stop a pending reveal on every backend.
changed enabled => {
if !root.enabled {
popup-open-timer.running = false;
root.popup-animation-enabled = false;
root.popup-progress = 0.0;
combo-popup.close();
}
}
animate popup-progress {
duration: 160ms;
easing: ease-out;
@@ -278,12 +289,15 @@ export component CompactComboBox inherits Rectangle {
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white;
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: root.subtle ? 12px : ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
option-touch := TouchArea {
enabled: root.enabled;
clicked => {
popup-open-timer.running = false;
root.popup-animation-enabled = false;
root.popup-progress = 0.0;
root.current-index = index;
root.selected(index);
if root.enabled && index >= 0 && index < root.model.length {
root.current-index = index;
root.selected(index);
}
combo-popup.close();
}
}
@@ -439,6 +453,8 @@ export component SecondaryButton inherits Rectangle {
export component ToggleSwitch inherits Rectangle {
in-out property <bool> checked;
in property <bool> enabled: true;
in property <bool> toggle-on-click: true;
callback clicked();
callback toggled(bool);
width: 36px;
@@ -459,8 +475,11 @@ export component ToggleSwitch inherits Rectangle {
TouchArea {
enabled: root.enabled;
clicked => {
root.checked = !root.checked;
root.toggled(root.checked);
if root.toggle-on-click {
root.checked = !root.checked;
root.toggled(root.checked);
}
root.clicked();
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Single AOT entry point. Generating one header for the entire UI avoids
// duplicate imported-component definitions when C++ uses all three windows.
export { AnnouncementItem, MessageStrings } from "message_panel.slint";
export { MainWindow, RecentConnection, UpdateCheckStatus, UiStrings } from "main_window.slint";
export { MainWindow, RecentConnection, UpdateCheckStatus, AutostartStatus, UiStrings } from "main_window.slint";
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
export { ServerWindow, ServerStatusBar, ServerStrings, ControllerEntry } from "server_window.slint";
+85 -2
View File
@@ -32,6 +32,7 @@ export struct ReleaseNoteBlock {
}
export enum UpdateCheckStatus { idle, checking, up-to-date, available, failed }
export enum AutostartStatus { checking, enabled, disabled, unsupported, failed }
enum CopyFeedback { idle, success, failed }
export global UiStrings {
@@ -401,6 +402,16 @@ export component MainWindow inherits Window {
}
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in property <bool> autostart-system-managed: false;
in property <AutostartStatus> autostart-status: AutostartStatus.checking;
out property <string> autostart-status-text:
root.autostart-status == AutostartStatus.checking ? GalleryStrings.tr("正在读取…", "Checking…", "Проверка…") :
root.autostart-status == AutostartStatus.enabled ? GalleryStrings.tr("已开启", "Enabled", "Включён") :
root.autostart-status == AutostartStatus.disabled ? GalleryStrings.tr("未开启", "Disabled", "Выключен") :
root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr("设备不支持", "Unsupported", "Не поддерживается") :
GalleryStrings.tr("读取失败", "Read failed", "Ошибка чтения");
in-out property <bool> autostart-guide-open: false;
changed autostart-guide-open => { if root.autostart-guide-open { focus-sink.focus(); } }
in-out property <bool> daemon-enabled: false;
in-out property <bool> save-remote-previews: true;
// Show the preference on every platform; only adapted hosts can change it.
@@ -427,6 +438,7 @@ export component MainWindow inherits Window {
callback recent-delete(string);
callback save-settings() -> bool;
callback cancel-settings;
callback refresh-autostart-status;
callback save-self-hosted-settings() -> bool;
callback cancel-self-hosted-settings;
callback browse-save-path;
@@ -494,7 +506,10 @@ export component MainWindow inherits Window {
root.gallery-frame-rate-draft = root.gallery-frame-rate-index;
root.gallery-bandwidth-draft = root.gallery-bandwidth-index;
root.gallery-action("settings-open", "");
if root.autostart-system-managed { root.refresh-autostart-status(); }
focus-sink.focus();
} else {
root.autostart-guide-open = false;
}
}
@@ -721,6 +736,10 @@ export component MainWindow inherits Window {
focus-sink := FocusScope {
x: 0px; y: 0px; width: 0px; height: 0px;
key-pressed(event) => {
if root.autostart-guide-open {
if event.text == Key.Return || event.text == Key.Escape { root.autostart-guide-open = false; }
return accept;
}
if root.wayland-notice-open {
if event.text == Key.Return || event.text == Key.Escape {
root.wayland-notice-open = false;
@@ -939,7 +958,7 @@ export component MainWindow inherits Window {
accessible-label: UiStrings.remote-id;
vertical-alignment: center;
selection-foreground-color: white; selection-background-color: WorkbenchStyle.accent;
edited => { root.connection-input-error = ""; remote-id-format-timer.restart(); }
edited => { root.connection-input-error = ""; remote-id-format-timer.running = true; }
accepted => { if !root.connection-pending { root.connect-requested(root.remote-id-input); } }
}
}
@@ -1550,7 +1569,12 @@ export component MainWindow inherits Window {
}
remote-id-format-timer := Timer {
interval: 1ms;
running: false;
triggered => {
// Use the running property for both transitions. restart() resumes
// the native repeating timer without restoring this property, so
// a later stop() cannot change it and the selection keeps resetting.
self.stop();
let formatted = root.format-remote-id(root.remote-id-input);
root.remote-id-input = formatted;
// IDs are ASCII-only, so character-count is also the UTF-8 offset
@@ -1963,7 +1987,13 @@ export component MainWindow inherits Window {
}
SettingsGroup {
title: GalleryStrings.tr("启动与后台", "Startup & background", "Запуск и фоновая работа"); rows: 2;
SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
if root.autostart-system-managed: SettingsToggleRow {
label: UiStrings.autostart;
checked: root.autostart-status == AutostartStatus.enabled;
toggle-on-click: false;
clicked => { root.autostart-guide-open = true; }
}
SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; }
}
}
@@ -1998,6 +2028,59 @@ export component MainWindow inherits Window {
}
}
if root.autostart-guide-open && root.settings-open: Rectangle {
background: #15223835; z: 49;
TouchArea { clicked => { focus-sink.focus(); } }
DialogSurface {
x: (parent.width - self.width) / 2; y: (parent.height - self.height) / 2;
width: min(460px, parent.width - 32px);
height: min(380px, parent.height - 24px);
background: #f8fafc; border-color: #e1e7ef;
drop-shadow-color: #14294424; drop-shadow-blur: 18px; drop-shadow-offset-y: 6px;
TouchArea { clicked => { focus-sink.focus(); } }
Text {
x: 24px; y: 18px; width: parent.width - 48px; height: 26px;
text: GalleryStrings.tr("设置开机自启动", "Set up auto start", "Настройка автозапуска");
font-size: 18px; font-weight: 600; color: WorkbenchStyle.text;
}
ScrollView {
x: 24px; y: 58px; width: parent.width - 48px; height: parent.height - 120px;
viewport-width: self.visible-width;
horizontal-scrollbar-policy: always-off;
viewport-height: guide-content.preferred-height;
guide-content := VerticalLayout {
width: parent.width; padding: 0px; spacing: 12px;
Text { text: root.autostart-status-text; color: WorkbenchStyle.accent; font-size: 13px; wrap: word-wrap; }
Text {
text: root.autostart-status == AutostartStatus.unsupported ? GalleryStrings.tr(
"当前设备不支持开机自启动。", "This device does not support auto start.", "Это устройство не поддерживает автозапуск.") :
root.autostart-status == AutostartStatus.failed ? GalleryStrings.tr(
"无法读取系统状态。请刷新重试,或在系统设置中核对。", "Could not read the system status. Retry or check it in Settings.", "Не удалось прочитать состояние. Повторите попытку или проверьте настройки системы.") :
GalleryStrings.tr("请在系统设置中添加或移除本应用,返回后会自动刷新状态。", "Add or remove this app in system Settings. The status refreshes when you return.", "Добавьте или удалите приложение в настройках системы. После возвращения статус обновится.");
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
if root.autostart-status != AutostartStatus.unsupported: Text {
text: GalleryStrings.tr(
"设置 → 应用和元服务 → 应用启动管理\n\n开启:点击“添加应用”,选择 CrossDesk,然后点击“完成”。\n\n关闭:在自启动列表中找到本应用,点击“移除”。",
"Settings → Apps & services → App startup management\n\nEnable: choose Add app, select CrossDesk, then choose Done.\n\nDisable: find this app in the startup list and choose Remove.",
"Настройки → Приложения и сервисы → Управление запуском\n\nВключить: добавьте CrossDesk в список и подтвердите выбор.\n\nВыключить: удалите приложение из списка автозапуска.");
color: WorkbenchStyle.text; font-size: 13px; wrap: word-wrap;
}
}
}
SettingsButton {
x: 24px; y: parent.height - 48px; width: 112px;
text: GalleryStrings.tr("刷新状态", "Refresh status", "Обновить");
enabled: root.autostart-status != AutostartStatus.checking;
clicked => { root.refresh-autostart-status(); }
}
SettingsButton {
x: parent.width - 116px; y: parent.height - 48px; width: 92px; primary: true;
text: UiStrings.ok; clicked => { root.autostart-guide-open = false; }
}
}
}
if root.self-host-settings-open: Rectangle {
background: #15223835;
z: 49;
+34
View File
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-15
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MODEL_UPDATE_H_
#define _MODEL_UPDATE_H_
#include <slint.h>
#include <utility>
#include <vector>
namespace crossdesk::ui {
// A reset invalidates row-count bindings even if the count is unchanged.
// Repeating it from a timer can continually restart dependent size animations.
template <typename Row>
void UpdateModelRows(slint::VectorModel<Row>& model, std::vector<Row> rows) {
if (model.row_count() != rows.size()) {
model.set_vector(std::move(rows));
return;
}
for (std::size_t i = 0; i < rows.size(); ++i) {
if (model.row_data(i) != rows[i]) {
model.set_row_data(i, rows[i]);
}
}
}
} // namespace crossdesk::ui
#endif
@@ -130,9 +130,13 @@ export component SettingsRow inherits Rectangle {
export component SettingsToggleRow inherits SettingsRow {
in-out property <bool> checked;
in property <bool> toggle-on-click: true;
callback clicked();
label-reserve: 64px;
ToggleSwitch {
x: parent.width - self.width - 12px; y: (parent.height - self.height) / 2;
checked <=> root.checked; enabled: root.enabled;
toggle-on-click: root.toggle-on-click;
clicked => { root.clicked(); }
}
}
+29 -6
View File
@@ -238,6 +238,11 @@ export component StreamWindow inherits Window {
callback reorder-tab(int, length, length);
callback close-tab(string);
callback switch-display(int);
in property <bool> secure-attention-supported: true;
private property <[string]> secure-shortcut-items: ["Ctrl+Alt+Del", "Win+L"];
private property <[string]> ordinary-shortcut-items: ["Win+L"];
private property <[string]> shortcut-items: root.secure-attention-supported ? root.secure-shortcut-items : root.ordinary-shortcut-items;
private property <length> shortcut-menu-height: root.shortcut-items.length * 30px + 8px;
callback send-shortcut(string);
callback toggle-mouse-control;
callback toggle-privacy;
@@ -265,6 +270,10 @@ export component StreamWindow inherits Window {
private property <length> stats-header-height: 18px;
private property <length> stats-content-height:
root.stats-header-height + root.stats-rows.length * root.stats-row-height + 65px;
private property <length> file-panel-x: 18px;
private property <length> file-panel-y: video.height - min(210px, video.height * 0.35);
private property <length> file-panel-width: min(430px, video.width * 0.6);
private property <length> file-panel-height: min(190px, video.height * 0.32);
private property <bool> display-menu-open: false;
private property <bool> display-menu-animation-enabled: false;
private property <float> display-menu-progress: 0.0;
@@ -392,11 +401,14 @@ export component StreamWindow inherits Window {
&& pointer-x >= shortcut-menu-x
&& pointer-x <= shortcut-menu-x + 180px
&& pointer-y >= shortcut-menu-y
&& pointer-y <= shortcut-menu-y + 68px;
&& pointer-y <= shortcut-menu-y + root.shortcut-menu-height;
let over-video-menu = root.video-menu-open
&& pointer-x >= root.video-menu-x && pointer-x <= root.video-menu-x + root.video-menu-width
&& pointer-y >= root.video-menu-y && pointer-y <= root.video-menu-y + root.video-menu-height;
return over-control || over-display-menu || over-shortcut-menu || over-video-menu;
let over-file-panel = root.file-transfer-visible
&& pointer-x >= root.file-panel-x && pointer-x <= root.file-panel-x + root.file-panel-width
&& pointer-y >= root.file-panel-y && pointer-y <= root.file-panel-y + root.file-panel-height;
return over-control || over-display-menu || over-shortcut-menu || over-video-menu || over-file-panel;
}
begin-control-drag(press-x, press-y) => {
@@ -1246,11 +1258,11 @@ export component StreamWindow inherits Window {
}
if root.shortcut-menu-open: Rectangle {
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: 68px;
x: control.x + shortcut-button.x; y: control.y + 40px; width: 180px; height: root.shortcut-menu-height;
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.menu; z: 11;
VerticalLayout {
padding: 4px; spacing: 1px;
for shortcut in ["Ctrl+Alt+Del", "Win+L"]: Rectangle {
for shortcut in root.shortcut-items: Rectangle {
height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent;
Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
shortcut-touch := TouchArea {
@@ -1365,10 +1377,21 @@ export component StreamWindow inherits Window {
// Transfer progress overlay at bottom-left.
if root.file-transfer-visible: Rectangle {
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
x: root.file-panel-x; y: root.file-panel-y;
width: root.file-panel-width; height: root.file-panel-height;
background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: UiRadius.surface; z: 7;
// Header/padding and drags leaving the panel belong to local UI.
TouchArea {
pointer-event(event) => {
if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; }
}
scroll-event(event) => { accept }
}
Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; }
IconButton { x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px; clicked => { root.close-file-transfer(); } }
IconButton {
x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px;
clicked => { root.suppress-next-remote-pointer-release = true; root.close-file-transfer(); }
}
ScrollView {
x: 10px; y: 32px; width: parent.width - 20px; height: parent.height - 42px;
VerticalLayout {