mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 01:46:47 +08:00
[feat] add HarmonyOS desktop and mobile clients,issue #117
This commit is contained in:
@@ -45,6 +45,9 @@ class KeyboardCapturer : public DeviceController {
|
||||
// Backends with an asynchronous capture loop override this so the UI can
|
||||
// fall back to window events if that loop stops unexpectedly.
|
||||
virtual bool IsHookActive() const { return true; }
|
||||
// Permission-denied backends can wait for an OS grant notification rather
|
||||
// than repeatedly releasing held fallback keys to retry an impossible hook.
|
||||
virtual bool ShouldRetryHook() const { return true; }
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false) = 0;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,11 +6,29 @@
|
||||
|
||||
#include "spdlog/sinks/rotating_file_sink.h"
|
||||
#include "spdlog/sinks/stdout_color_sinks.h"
|
||||
#if defined(__OHOS__)
|
||||
#include <hilog/log.h>
|
||||
#include "spdlog/sinks/base_sink.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
#if defined(__OHOS__)
|
||||
class HarmonyLogSink final : public spdlog::sinks::base_sink<std::mutex> {
|
||||
void sink_it_(const spdlog::details::log_msg& message) override {
|
||||
spdlog::memory_buf_t formatted;
|
||||
formatter_->format(message, formatted);
|
||||
const std::string text(formatted.data(), formatted.size());
|
||||
const auto level = message.level >= spdlog::level::err ? LOG_ERROR :
|
||||
message.level >= spdlog::level::warn ? LOG_WARN : LOG_INFO;
|
||||
OH_LOG_Print(LOG_APP, level, 0, "CrossDeskDesktop", "%{public}s", text.c_str());
|
||||
}
|
||||
void flush_() override {}
|
||||
};
|
||||
#endif
|
||||
|
||||
std::string g_log_dir = "logs";
|
||||
std::string g_logger_name = LOGGER_NAME;
|
||||
std::once_flag g_logger_once_flag;
|
||||
@@ -56,7 +74,11 @@ std::shared_ptr<spdlog::logger> get_logger() {
|
||||
g_log_dir + "/" + g_logger_name + ".log", 5 * 1024 * 1024, 3);
|
||||
|
||||
std::vector<spdlog::sink_ptr> sinks;
|
||||
#if defined(__OHOS__)
|
||||
sinks.push_back(std::make_shared<HarmonyLogSink>());
|
||||
#else
|
||||
sinks.push_back(std::make_shared<spdlog::sinks::stdout_color_sink_mt>());
|
||||
#endif
|
||||
sinks.push_back(file_sink);
|
||||
|
||||
auto logger = std::make_shared<spdlog::logger>(g_logger_name, sinks.begin(),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _HARMONY_TRAY_H_
|
||||
#define _HARMONY_TRAY_H_
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
// Constructed and invoked on the shared Slint event thread.
|
||||
class HarmonyTray {
|
||||
public:
|
||||
HarmonyTray(std::function<void()> show, std::function<void()> hide,
|
||||
std::function<void()> settings, std::function<void()> exit,
|
||||
const std::string& tooltip, int language);
|
||||
~HarmonyTray();
|
||||
bool MinimizeToTray();
|
||||
void RemoveTrayIcon();
|
||||
static void Dispatch(const std::string& action);
|
||||
private:
|
||||
std::function<void()> show_, hide_, settings_, exit_;
|
||||
bool active_ = false;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOW_STATE_H_
|
||||
#define _WINDOW_STATE_H_
|
||||
|
||||
namespace slint { class Window; }
|
||||
namespace crossdesk {
|
||||
// GUI-thread query of the native window focus delivered by Harmony ArkUI.
|
||||
bool IsHarmonyWindowActive(const slint::Window& window);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _HARMONY_AUTOSTART_H_
|
||||
#define _HARMONY_AUTOSTART_H_
|
||||
|
||||
namespace crossdesk {
|
||||
enum class HarmonyAutostartStatus { checking, enabled, disabled, unsupported, failed };
|
||||
|
||||
// ArkTS publishes system state; the GUI reads it without waiting on a service.
|
||||
HarmonyAutostartStatus GetHarmonyAutostartStatus();
|
||||
void SetHarmonyAutostartStatus(HarmonyAutostartStatus status);
|
||||
void RefreshHarmonyAutostartStatus();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SYSTEM_SHORTCUTS_H_
|
||||
#define _SYSTEM_SHORTCUTS_H_
|
||||
|
||||
#include <remote_action.h>
|
||||
namespace crossdesk {
|
||||
bool SendHarmonySystemShortcut(ServiceCommandFlag command);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -57,7 +57,7 @@ std::unique_ptr<PrivacyBackend> CreateMacPrivacyBackend();
|
||||
// Flush queued AppKit cleanup after joining the controller at app shutdown.
|
||||
void FlushMacPrivacyTasks();
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#if defined(__linux__) && !defined(__OHOS__)
|
||||
std::unique_ptr<PrivacyBackend> CreateLinuxPrivacyBackend();
|
||||
#endif
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ PrivacyController::PrivacyController(Factory factory) {
|
||||
if (!factory) factory = CreateWindowsPrivacyBackend;
|
||||
#elif defined(__APPLE__)
|
||||
if (!factory) factory = CreateMacPrivacyBackend;
|
||||
#elif defined(__linux__)
|
||||
#elif (defined(__linux__) && !defined(__OHOS__))
|
||||
if (!factory) factory = CreateLinuxPrivacyBackend;
|
||||
#endif
|
||||
thread_ = std::thread([this, factory] { Run(factory); });
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
#include <cstdlib>
|
||||
#include "rd_log.h"
|
||||
#if defined(__OHOS__)
|
||||
#include "platform/path_backend.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -18,6 +21,11 @@ FileReceiver::FileReceiver(const std::filesystem::path& output_dir)
|
||||
}
|
||||
|
||||
std::filesystem::path FileReceiver::GetDefaultDesktopPath() {
|
||||
#if defined(__OHOS__)
|
||||
// Harmony has no process HOME/Desktop. Use writable application storage
|
||||
// until the user selects and authorizes a public directory in settings.
|
||||
const auto desktop_path = platform::GetCachePath("CrossDesk") / "Received Files";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
const char* home_env = std::getenv("USERPROFILE");
|
||||
#else
|
||||
@@ -29,6 +37,7 @@ std::filesystem::path FileReceiver::GetDefaultDesktopPath() {
|
||||
|
||||
std::filesystem::path desktop_path =
|
||||
std::filesystem::path(home_env) / "Desktop";
|
||||
#endif
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(desktop_path, ec);
|
||||
|
||||
@@ -122,7 +122,11 @@ std::optional<VersionInfo> CheckUpdate() {
|
||||
const auto manifest = nlohmann::json::parse(res->body);
|
||||
auto info = ParseVersionInfo(manifest);
|
||||
if (info) {
|
||||
#if defined(__linux__)
|
||||
#if defined(__OHOS__)
|
||||
// A Linux package cannot be installed by Harmony's package manager.
|
||||
// Until the manifest has a Harmony artifact, use the release website.
|
||||
info->download_url = "https://crossdesk.cn";
|
||||
#elif defined(__linux__)
|
||||
std::string architecture;
|
||||
#if defined(__x86_64__)
|
||||
architecture = "amd64";
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
#include "crossdesk_ui.h"
|
||||
#include "slint_render_test_platform.h"
|
||||
#include "ui/model_update.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
bool Check(bool value, const char* message) {
|
||||
if (!value) std::cerr << message << '\n';
|
||||
return value;
|
||||
}
|
||||
|
||||
bool VerifyStreamPopups(RenderTestPlatform& platform) {
|
||||
auto stream = crossdesk::ui::StreamWindow::create();
|
||||
auto& screen = *platform.adapter;
|
||||
stream->window().set_size(slint::LogicalSize({1280, 720}));
|
||||
stream->set_video_settings_enabled(true);
|
||||
stream->show();
|
||||
const int top = int(stream->get_video_area_top());
|
||||
auto displays = std::make_shared<slint::VectorModel<slint::SharedString>>();
|
||||
auto transfers = std::make_shared<slint::VectorModel<crossdesk::ui::FileTransferEntry>>();
|
||||
stream->set_displays(displays);
|
||||
stream->set_file_transfers(transfers);
|
||||
bool file_visible = false;
|
||||
unsigned ticks = 0;
|
||||
slint::Timer sync;
|
||||
sync.start(slint::TimerMode::Repeated, 16ms, [&] {
|
||||
// Match the live session's model synchronization, including long clicks.
|
||||
crossdesk::ui::UpdateModelRows(*displays,
|
||||
std::vector<slint::SharedString>{"Display 1", "Display 2", "Display 3"});
|
||||
crossdesk::ui::FileTransferEntry entry{};
|
||||
entry.name = "test.txt"; entry.progress = (++ticks % 100) / 100.f;
|
||||
crossdesk::ui::UpdateModelRows(*transfers, std::vector(8, entry));
|
||||
stream->set_file_transfer_visible(file_visible);
|
||||
});
|
||||
int remote_presses = 0, remote_releases = 0, remote_scrolls = 0;
|
||||
stream->on_pointer_input([&](auto, auto kind, float, float) {
|
||||
if (kind == slint::PointerEventKind::Down) ++remote_presses;
|
||||
if (kind == slint::PointerEventKind::Up) ++remote_releases;
|
||||
});
|
||||
stream->on_scroll_input([&](float, float, float, float) { ++remote_scrolls; });
|
||||
int selected_display = -1;
|
||||
stream->on_switch_display([&](int index) { selected_display = index; });
|
||||
std::string shortcut;
|
||||
stream->on_send_shortcut([&](slint::SharedString text) { shortcut = text; });
|
||||
int video_changes = 0;
|
||||
stream->on_change_video_settings([&](int quality, int rate, int policy) {
|
||||
stream->set_video_quality_index(quality);
|
||||
stream->set_frame_rate_index(rate);
|
||||
stream->set_adaptation_policy_index(policy);
|
||||
++video_changes;
|
||||
});
|
||||
stream->on_select_file([&] { file_visible = true; });
|
||||
bool close_synchronously = false;
|
||||
stream->on_close_file_transfer([&] {
|
||||
// The real application hides this on its next synchronization tick.
|
||||
file_visible = false;
|
||||
if (close_synchronously) stream->set_file_transfer_visible(false);
|
||||
});
|
||||
screen.Settle();
|
||||
for (int repeat = 0; repeat < 2; ++repeat) {
|
||||
screen.Click(21, top + 19);
|
||||
if (!Check(screen.Light(12, top + 128), "Display menu must fully open without pointer movement")) return false;
|
||||
screen.Click(60, top + 86, 4);
|
||||
if (!Check(selected_display == 1 && screen.Black(12, top + 128),
|
||||
"Display selection must survive live refresh and close the menu")) return false;
|
||||
screen.Click(53, top + 19);
|
||||
if (!Check(screen.Light(44, top + 102), "Shortcut menu must open without pointer movement")) return false;
|
||||
screen.Click(70, top + 88, 4);
|
||||
if (!Check(shortcut == "Win+L" && screen.Black(44, top + 102),
|
||||
"Shortcut selection must close the menu immediately")) return false;
|
||||
screen.Click(213, top + 19);
|
||||
if (!Check(stream->get_video_menu_open() && screen.Light(203, top + 210),
|
||||
"Video settings menu must fully open without pointer movement")) return false;
|
||||
screen.Click(240, top + 80, 4);
|
||||
screen.Click(900, top + 300);
|
||||
if (!Check(!stream->get_video_menu_open() && video_changes == repeat + 1,
|
||||
"Video selection and outside dismissal must respond immediately")) return false;
|
||||
}
|
||||
if (!Check(remote_presses == 0 && remote_releases == 0 && remote_scrolls == 0,
|
||||
"Menu gestures must not be forwarded to the peer")) return false;
|
||||
screen.Click(181, top + 19);
|
||||
const int panel_y = top + int(stream->get_video_area_height()) - 210;
|
||||
if (!Check(file_visible && screen.Light(20, panel_y + 25),
|
||||
"File progress must appear without pointer movement")) return false;
|
||||
// Header/padding is outside the list's own input handler.
|
||||
screen.Click(25, panel_y + 20, 4);
|
||||
stream->window().dispatch_pointer_scroll_event(
|
||||
slint::LogicalPosition({25, float(panel_y + 20)}), 0, -60);
|
||||
screen.Settle();
|
||||
if (!Check(remote_presses == 0 && remote_releases == 0 && remote_scrolls == 0,
|
||||
"File progress header and padding must consume input locally")) return false;
|
||||
const slint::LogicalPosition panel_point({25, float(panel_y + 20)});
|
||||
const slint::LogicalPosition outside({900, float(top + 300)});
|
||||
stream->window().dispatch_pointer_press_event(panel_point, slint::PointerEventButton::Left);
|
||||
screen.Settle(4);
|
||||
stream->window().dispatch_pointer_move_event(outside);
|
||||
stream->window().dispatch_pointer_release_event(outside, slint::PointerEventButton::Left);
|
||||
screen.Settle();
|
||||
if (!Check(remote_presses == 0 && remote_releases == 0,
|
||||
"Dragging out of the file panel must not release a button on the peer")) return false;
|
||||
screen.Click(430, panel_y + 16, 4);
|
||||
if (!Check(!file_visible && screen.Black(20, panel_y + 25),
|
||||
"File progress close must clear its pixels without pointer movement")) return false;
|
||||
if (!Check(remote_presses == 0 && remote_releases == 0,
|
||||
"Closing file progress must not send an orphan pointer release")) return false;
|
||||
close_synchronously = true;
|
||||
screen.Click(181, top + 19);
|
||||
screen.Click(430, panel_y + 16, 4);
|
||||
if (!Check(!file_visible && screen.Black(20, panel_y + 25) && remote_releases == 0,
|
||||
"Synchronous file panel dismissal must consume its closing release")) return false;
|
||||
screen.Click(900, top + 300);
|
||||
return Check(remote_presses == 1 && remote_releases == 1,
|
||||
"Closing local menus must restore remote input");
|
||||
}
|
||||
|
||||
bool VerifyWorkbenchPopups(RenderTestPlatform& platform) {
|
||||
auto main = crossdesk::ui::MainWindow::create();
|
||||
auto& screen = *platform.adapter;
|
||||
const float width = main->get_minimum_window_width();
|
||||
const float height = main->get_minimum_window_height();
|
||||
main->window().set_size(slint::LogicalSize({width, height}));
|
||||
main->show();
|
||||
auto groups = std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector<slint::SharedString>{"All", "Ungrouped", "Lab"});
|
||||
main->set_recent_groups(groups);
|
||||
int selected_group = -1;
|
||||
main->on_recent_group_selected([&](int index) { selected_group = index; });
|
||||
screen.Settle();
|
||||
const auto filter_closed = screen.Region(width - 170, 302, 140, 70);
|
||||
screen.Click(width - 90, 276);
|
||||
if (!Check(screen.Region(width - 170, 302, 140, 70) != filter_closed,
|
||||
"Group filter must reveal its contents while the pointer stays on the trigger")) return false;
|
||||
screen.Key(slint::platform::key_codes::Escape);
|
||||
if (!Check(screen.Region(width - 170, 302, 140, 70) == filter_closed,
|
||||
"Escape must dismiss the group filter")) return false;
|
||||
screen.Click(width - 90, 276);
|
||||
screen.Click(width - 90, 371, 4);
|
||||
if (!Check(selected_group == 2 && main->get_recent_group_index() == 2,
|
||||
"Group selection must survive a held click")) return false;
|
||||
screen.Click(width - 38, 20);
|
||||
screen.Click(width - 78, 56, 4);
|
||||
if (!Check(main->get_settings_open(), "Application menu must open settings on click")) return false;
|
||||
const float settings_y = (height - 446) / 2;
|
||||
screen.Click(width / 2 + 87, settings_y + 76, 4);
|
||||
if (!Check(main->get_settings_tab() == 1, "Settings tabs must switch without pointer movement")) return false;
|
||||
screen.Click(width / 2 - 87, settings_y + 76, 4);
|
||||
if (!Check(main->get_settings_tab() == 0, "Settings tabs must switch back immediately")) return false;
|
||||
// The first row uses the same CompactComboBox as codec/capture settings.
|
||||
const int combo_x = int(width / 2 + 100);
|
||||
const int combo_y = int(settings_y + 150);
|
||||
const int popup_y = int(settings_y + 164);
|
||||
const auto combo_closed = screen.Region(combo_x - 35, popup_y + 2, 60, 60);
|
||||
screen.Click(combo_x, combo_y);
|
||||
if (!Check(screen.Region(combo_x - 35, popup_y + 2, 60, 60) != combo_closed,
|
||||
"Settings dropdown must animate open without pointer movement")) return false;
|
||||
screen.Key(slint::platform::key_codes::Escape);
|
||||
if (!Check(screen.Region(combo_x - 35, popup_y + 2, 60, 60) == combo_closed,
|
||||
"Escape must dismiss the settings dropdown")) return false;
|
||||
screen.Click(combo_x, combo_y);
|
||||
screen.Click(combo_x, popup_y + 34, 4);
|
||||
if (!Check(main->get_language_index() == 1, "Settings dropdown must apply a selected row")) return false;
|
||||
// A session starting while a codec menu is open disables the setting.
|
||||
// Existing popup rows must not remain actionable after that transition.
|
||||
const int codec_y = int(settings_y + 224);
|
||||
screen.Click(combo_x, codec_y);
|
||||
main->set_settings_session_active(true);
|
||||
screen.Settle();
|
||||
screen.Click(combo_x, codec_y + 48, 4);
|
||||
if (!Check(main->get_codec_index() == 0,
|
||||
"A disabled codec dropdown must close and reject its stale options")) return false;
|
||||
main->set_settings_session_active(false);
|
||||
screen.Click(width / 2 + 32, settings_y + 412);
|
||||
if (!Check(!main->get_settings_open(), "Settings Cancel must close the dialog immediately")) return false;
|
||||
screen.Click(width - 38, 20);
|
||||
screen.Click(width - 78, 86, 4);
|
||||
if (!Check(main->get_messages_open(), "Messages must open directly from the application menu")) return false;
|
||||
screen.Key(slint::platform::key_codes::Escape);
|
||||
if (!Check(!main->get_messages_open(), "Escape must dismiss Messages")) return false;
|
||||
screen.Click(width - 38, 20);
|
||||
screen.Click(width - 78, 132, 4);
|
||||
if (!Check(main->get_about_open(), "About must open directly from the application menu")) return false;
|
||||
screen.Key(slint::platform::key_codes::Escape);
|
||||
return Check(!main->get_about_open(), "Escape must dismiss About");
|
||||
}
|
||||
|
||||
bool VerifyAutostartSettings(RenderTestPlatform& platform) {
|
||||
auto main = crossdesk::ui::MainWindow::create();
|
||||
auto& screen = *platform.adapter;
|
||||
using Status = crossdesk::ui::AutostartStatus;
|
||||
const float width = main->get_minimum_window_width();
|
||||
const float height = main->get_minimum_window_height();
|
||||
main->window().set_size(slint::LogicalSize({width, height}));
|
||||
main->set_autostart_system_managed(true);
|
||||
main->set_autostart_enabled(true); // Stale app preference must not label system state.
|
||||
main->set_autostart_status(Status::Disabled);
|
||||
int refreshes = 0;
|
||||
main->on_refresh_autostart_status([&] { ++refreshes; });
|
||||
main->show(); main->set_settings_open(true); screen.Settle();
|
||||
if (!Check(refreshes == 1, "Opening managed autostart settings must query the system")) return false;
|
||||
const float top = (height - 446) / 2;
|
||||
main->window().dispatch_pointer_scroll_event(slint::LogicalPosition({width / 2, top + 260}), 0, -1200);
|
||||
screen.Settle();
|
||||
const float toggle_x = width / 2 + 144, toggle_y = top + 322;
|
||||
const auto toggle_pixels = [&] {
|
||||
return screen.Region(int(toggle_x - 18), int(toggle_y - 10), 36, 20);
|
||||
};
|
||||
const auto save_snapshot = [&](const std::string& suffix) {
|
||||
if (const char* snapshot = std::getenv("CROSSDESK_AUTOSTART_UI_SNAPSHOT")) {
|
||||
std::ofstream out(std::string(snapshot) + suffix, std::ios::binary);
|
||||
out << "P6\n" << screen.extent.width << ' ' << screen.extent.height << "\n255\n";
|
||||
for (const auto pixel : screen.pixels) { out.put(pixel.r); out.put(pixel.g); out.put(pixel.b); }
|
||||
}
|
||||
};
|
||||
const auto disabled = toggle_pixels();
|
||||
save_snapshot(".settings.ppm");
|
||||
main->set_autostart_status(Status::Enabled); screen.Settle();
|
||||
const auto enabled = toggle_pixels();
|
||||
if (!Check(enabled != disabled,
|
||||
"The autostart switch must reflect system changes while settings stay open")) return false;
|
||||
main->set_autostart_status(Status::Disabled); screen.Settle();
|
||||
screen.Click(toggle_x, toggle_y);
|
||||
if (!Check(main->get_autostart_guide_open() && main->get_autostart_status() == Status::Disabled &&
|
||||
main->get_autostart_enabled(),
|
||||
"Clicking the disabled system switch opens guidance without changing state or preference")) return false;
|
||||
screen.Key(slint::platform::key_codes::Escape);
|
||||
if (!Check(toggle_pixels() == disabled, "The system switch must not flip when guidance closes")) return false;
|
||||
screen.Click(toggle_x, toggle_y);
|
||||
// Both actions stay below the scrollable instructions at minimum size.
|
||||
const auto button_y = [&]() {
|
||||
const int x = int(width / 2 + 120), y = int(height / 2 + 158);
|
||||
const auto p = screen.pixels.at(y * screen.extent.width + x);
|
||||
return p.b > 180 && p.r < 90 && p.g > 80 && p.g < 160 ? y : -1;
|
||||
};
|
||||
int refresh_y = button_y();
|
||||
if (!Check(refresh_y > 0, "Autostart guide buttons must fit inside the minimum window")) return false;
|
||||
save_snapshot("");
|
||||
screen.Click(width / 2 - 145, refresh_y);
|
||||
if (!Check(refreshes == 2, "Autostart guide Refresh must query the actual system")) return false;
|
||||
main->set_autostart_status(Status::Checking); screen.Settle();
|
||||
screen.Click(width / 2 - 145, button_y());
|
||||
if (!Check(refreshes == 2, "Refresh is disabled while the system query is pending")) return false;
|
||||
for (const auto status : {Status::Failed, Status::Unsupported, Status::Enabled}) {
|
||||
main->set_autostart_status(status); screen.Settle();
|
||||
if (!Check(button_y() > 0, "Error and unsupported states must remain readable and dismissible")) return false;
|
||||
}
|
||||
screen.Key(slint::platform::key_codes::Escape);
|
||||
if (!Check(!main->get_autostart_guide_open() && main->get_settings_open(),
|
||||
"Escape closes the guide without cancelling other settings")) return false;
|
||||
if (!Check(toggle_pixels() == enabled, "The switch must still follow system state after opening guidance")) return false;
|
||||
screen.Click(toggle_x, toggle_y);
|
||||
if (!Check(main->get_autostart_guide_open() && main->get_autostart_status() == Status::Enabled,
|
||||
"Clicking the enabled system switch must also open guidance without changing state")) return false;
|
||||
screen.Click(width / 2 + 160, button_y());
|
||||
if (!Check(!main->get_autostart_guide_open(), "The guide confirmation must close immediately")) return false;
|
||||
if (!Check(toggle_pixels() == enabled, "Dismissing guidance must keep the system switch enabled")) return false;
|
||||
main->set_autostart_guide_open(true); main->set_settings_open(false); screen.Settle();
|
||||
if (!Check(!main->get_autostart_guide_open(), "Closing settings must also dismiss its guide")) return false;
|
||||
main->set_autostart_system_managed(false);
|
||||
main->set_settings_open(true); screen.Settle();
|
||||
main->window().dispatch_pointer_scroll_event(slint::LogicalPosition({width / 2, top + 260}), 0, -1200);
|
||||
screen.Settle();
|
||||
screen.Click(toggle_x, toggle_y);
|
||||
return Check(refreshes == 2 && !main->get_autostart_enabled() && !main->get_autostart_guide_open(),
|
||||
"Other platforms toggle their autostart preference directly without querying Harmony or opening guidance");
|
||||
}
|
||||
|
||||
bool VerifyGalleryPopups(RenderTestPlatform& platform) {
|
||||
auto wall = crossdesk::ui::MainWindow::create();
|
||||
auto& screen = *platform.adapter;
|
||||
wall->set_gallery_mode(true);
|
||||
wall->window().set_size(slint::LogicalSize({900, 600}));
|
||||
wall->set_gallery_count(8);
|
||||
wall->set_gallery_connected(8);
|
||||
wall->set_gallery_groups(std::make_shared<slint::VectorModel<slint::SharedString>>(
|
||||
std::vector<slint::SharedString>{"All", "Lab"}));
|
||||
auto network = std::make_shared<slint::VectorModel<crossdesk::ui::GalleryNetworkDevice>>();
|
||||
wall->set_gallery_network_devices(network);
|
||||
wall->show();
|
||||
unsigned ticks = 0;
|
||||
slint::Timer sync;
|
||||
sync.start(slint::TimerMode::Repeated, 16ms, [&] {
|
||||
crossdesk::ui::GalleryNetworkDevice row{};
|
||||
row.id = "123456789"; row.name = "Test device";
|
||||
row.inbound = slint::SharedString(std::to_string(++ticks) + " kbps");
|
||||
crossdesk::ui::UpdateModelRows(*network, std::vector(8, row));
|
||||
});
|
||||
screen.Settle();
|
||||
const auto baseline = screen.Region(75, 100, 740, 400);
|
||||
for (int repeat = 0; repeat < 2; ++repeat) {
|
||||
screen.Click(160, 578);
|
||||
if (!Check(wall->get_gallery_network_open() && screen.Region(75, 100, 740, 400) != baseline,
|
||||
"DeskHub network dialog must paint immediately during live refresh")) return false;
|
||||
if (repeat == 0) screen.Click(780, 102, 4);
|
||||
else screen.Key(slint::platform::key_codes::Escape);
|
||||
if (!Check(!wall->get_gallery_network_open() && screen.Region(75, 100, 740, 400) == baseline,
|
||||
"DeskHub network dialog must close completely without pointer movement")) return false;
|
||||
}
|
||||
// Open the add menu, leave the pointer on it, then select its first row.
|
||||
const auto menu_closed = screen.Region(490, 42, 220, 60);
|
||||
screen.Click(680, 24);
|
||||
if (!Check(screen.Region(490, 42, 220, 60) != menu_closed,
|
||||
"DeskHub add menu must appear without pointer movement")) return false;
|
||||
screen.Click(600, 55, 4);
|
||||
if (!Check(wall->get_gallery_adding(), "Add menu must open the device dialog")) return false;
|
||||
screen.Click(458, 421, 4);
|
||||
return Check(!wall->get_gallery_adding(), "Add dialog Cancel must respond immediately");
|
||||
}
|
||||
|
||||
bool VerifyServerPanel(RenderTestPlatform& platform) {
|
||||
auto panel = crossdesk::ui::ServerWindow::create();
|
||||
auto& screen = *platform.adapter;
|
||||
panel->set_controllers(std::make_shared<slint::VectorModel<crossdesk::ui::ControllerEntry>>(
|
||||
std::vector<crossdesk::ui::ControllerEntry>{{"1", "First"}, {"2", "Second"}}));
|
||||
panel->window().set_size(slint::LogicalSize({225, panel->get_content_height()}));
|
||||
panel->show();
|
||||
int selected = -1, disconnected = 0, picked = 0;
|
||||
panel->on_controller_selected([&](int index) { selected = index; });
|
||||
panel->on_disconnect_all([&] { ++disconnected; });
|
||||
panel->on_select_file([&] { ++picked; panel->set_file_transfer_visible(true); });
|
||||
slint::Timer sync;
|
||||
sync.start(slint::TimerMode::Repeated, 16ms, [&] {
|
||||
const auto height = unsigned(panel->get_content_height());
|
||||
if (screen.extent.height != height)
|
||||
panel->window().set_size(slint::LogicalSize({225, float(height)}));
|
||||
panel->set_file_progress(0.5);
|
||||
});
|
||||
screen.Settle();
|
||||
screen.Click(50, 84, 4);
|
||||
if (!Check(selected == 1, "Controlled-side controller selection must respond to a held click")) return false;
|
||||
const auto normal_height = screen.extent.height;
|
||||
screen.Click(180, 123, 4);
|
||||
if (!Check(panel->get_confirming_disconnect() && screen.extent.height == normal_height + 36,
|
||||
"Disconnect confirmation must expand immediately")) return false;
|
||||
screen.Click(110, 123, 4);
|
||||
if (!Check(!panel->get_confirming_disconnect() && disconnected == 0 && screen.extent.height == normal_height,
|
||||
"Cancel must collapse the controlled-side confirmation")) return false;
|
||||
screen.Click(180, 123, 4);
|
||||
screen.Click(180, 123, 4);
|
||||
if (!Check(disconnected == 1 && !panel->get_confirming_disconnect(),
|
||||
"Disconnect all must require and accept confirmation")) return false;
|
||||
screen.Click(110, 123, 4);
|
||||
return Check(picked == 1 && screen.extent.height == normal_height + 58,
|
||||
"Controlled-side file progress must resize the panel immediately");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
auto platform = std::make_unique<RenderTestPlatform>();
|
||||
auto* backend = platform.get();
|
||||
slint::platform::set_platform(std::move(platform));
|
||||
if (!VerifyStreamPopups(*backend)) return 1;
|
||||
if (!VerifyWorkbenchPopups(*backend)) return 2;
|
||||
if (!VerifyGalleryPopups(*backend)) return 3;
|
||||
if (!VerifyServerPanel(*backend)) return 4;
|
||||
if (!VerifyAutostartSettings(*backend)) return 5;
|
||||
std::cout << "Slint popup rendering and local input passed.\n";
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-10-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SLINT_RENDER_TEST_PLATFORM_H_
|
||||
#define _SLINT_RENDER_TEST_PLATFORM_H_
|
||||
|
||||
#include <slint-platform.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
// Retain pixels and honor redraw notifications, as the Harmony adapter does.
|
||||
// take_snapshot() forces a repaint and can conceal a stalled popup/animation.
|
||||
class RenderTestWindow : public slint::platform::WindowAdapter {
|
||||
public:
|
||||
slint::platform::SoftwareRenderer software{
|
||||
slint::platform::SoftwareRenderer::RepaintBufferType::ReusedBuffer};
|
||||
slint::PhysicalSize extent{{1280, 720}};
|
||||
std::vector<slint::Rgb8Pixel> pixels = std::vector<slint::Rgb8Pixel>(1280 * 720);
|
||||
bool dirty = true;
|
||||
slint::PhysicalSize size() override { return extent; }
|
||||
slint::platform::AbstractRenderer& renderer() override { return software; }
|
||||
void request_redraw() override { dirty = true; }
|
||||
void set_size(slint::PhysicalSize value) override {
|
||||
extent = value;
|
||||
pixels.resize(extent.width * extent.height);
|
||||
window().dispatch_resize_event(slint::LogicalSize(
|
||||
{float(extent.width), float(extent.height)}));
|
||||
dirty = true;
|
||||
}
|
||||
void Render() {
|
||||
if (!dirty && !window().has_active_animations()) return;
|
||||
dirty = false;
|
||||
software.render(pixels, extent.width);
|
||||
}
|
||||
void Settle(int frames = 15) {
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||
slint::platform::update_timers_and_animations();
|
||||
Render();
|
||||
}
|
||||
}
|
||||
void Click(float x, float y, int hold_frames = 0) {
|
||||
const slint::LogicalPosition point({x, y});
|
||||
window().dispatch_pointer_move_event(point);
|
||||
window().dispatch_pointer_press_event(point, slint::PointerEventButton::Left);
|
||||
Settle(hold_frames);
|
||||
window().dispatch_pointer_release_event(point, slint::PointerEventButton::Left);
|
||||
Settle(); // Do not move the pointer to make a delayed repaint pass.
|
||||
}
|
||||
bool Light(int x, int y) const {
|
||||
const auto p = pixels.at(y * extent.width + x);
|
||||
return p.r >= 230 && p.g >= 230 && p.b >= 230;
|
||||
}
|
||||
bool Black(int x, int y) const {
|
||||
const auto p = pixels.at(y * extent.width + x);
|
||||
return p.r == 0 && p.g == 0 && p.b == 0;
|
||||
}
|
||||
std::uint64_t Region(int x, int y, int width, int height) const {
|
||||
std::uint64_t hash = 14695981039346656037ull;
|
||||
for (int row = y; row < y + height; ++row)
|
||||
for (int col = x; col < x + width; ++col) {
|
||||
const auto p = pixels.at(row * extent.width + col);
|
||||
for (auto channel : {p.r, p.g, p.b}) hash = (hash ^ channel) * 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
void Key(const slint::SharedString& key) {
|
||||
window().dispatch_key_press_event(key);
|
||||
window().dispatch_key_release_event(key);
|
||||
Settle();
|
||||
}
|
||||
};
|
||||
|
||||
class RenderTestPlatform : public slint::platform::Platform {
|
||||
public:
|
||||
RenderTestWindow* adapter = nullptr;
|
||||
std::unique_ptr<slint::platform::WindowAdapter> create_window_adapter() override {
|
||||
auto result = std::make_unique<RenderTestWindow>();
|
||||
adapter = result.get();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,112 @@
|
||||
#include "crossdesk_ui.h"
|
||||
#include "ui/model_update.h"
|
||||
|
||||
#include <slint-platform.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
// Match the retained software buffer used by the Harmony desktop adapter.
|
||||
// A full window snapshot would conceal missing damage/redraw notifications.
|
||||
class TestWindow : public slint::platform::WindowAdapter {
|
||||
public:
|
||||
slint::platform::SoftwareRenderer software{
|
||||
slint::platform::SoftwareRenderer::RepaintBufferType::ReusedBuffer};
|
||||
std::vector<slint::Rgb8Pixel> pixels = std::vector<slint::Rgb8Pixel>(1280 * 720);
|
||||
bool dirty = true;
|
||||
slint::PhysicalSize size() override { return slint::PhysicalSize({1280, 720}); }
|
||||
slint::platform::AbstractRenderer& renderer() override { return software; }
|
||||
void request_redraw() override { dirty = true; }
|
||||
void Render() {
|
||||
if (!dirty && !window().has_active_animations()) return;
|
||||
dirty = false;
|
||||
software.render(pixels, 1280);
|
||||
}
|
||||
};
|
||||
|
||||
class TestPlatform : public slint::platform::Platform {
|
||||
public:
|
||||
TestWindow* adapter = nullptr;
|
||||
std::unique_ptr<slint::platform::WindowAdapter> create_window_adapter() override {
|
||||
auto result = std::make_unique<TestWindow>();
|
||||
adapter = result.get();
|
||||
return result;
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
auto platform = std::make_unique<TestPlatform>();
|
||||
auto* backend = platform.get();
|
||||
slint::platform::set_platform(std::move(platform));
|
||||
auto stream = crossdesk::ui::StreamWindow::create();
|
||||
auto& adapter = *backend->adapter;
|
||||
stream->window().dispatch_resize_event(slint::LogicalSize({1280, 720}));
|
||||
stream->show();
|
||||
auto rows = std::make_shared<slint::VectorModel<crossdesk::ui::NetworkStatsRow>>();
|
||||
stream->set_stats_rows(rows);
|
||||
std::vector<crossdesk::ui::NetworkStatsRow> stats(4);
|
||||
unsigned samples = 0;
|
||||
bool stats_visible = false;
|
||||
stream->on_toggle_stats([&] { stats_visible = !stats_visible; });
|
||||
// Synchronize inside a Slint timer, exactly as GuiApplication does. Updating
|
||||
// before update_timers_and_animations would use the previous animation tick.
|
||||
slint::Timer sync;
|
||||
sync.start(slint::TimerMode::Repeated, std::chrono::milliseconds(16), [&] {
|
||||
stream->set_stats_visible(stats_visible);
|
||||
// Keep values changing while the panel animates, as during a live session.
|
||||
stats[0].inbound = slint::SharedString(std::to_string(++samples) + " kbps");
|
||||
crossdesk::ui::UpdateModelRows(*rows, stats);
|
||||
});
|
||||
const auto tick = [&] {
|
||||
slint::platform::update_timers_and_animations();
|
||||
adapter.Render();
|
||||
};
|
||||
const auto settle = [&](int frames = 12) {
|
||||
for (int i = 0; i < frames; ++i) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(16));
|
||||
tick();
|
||||
}
|
||||
};
|
||||
settle();
|
||||
const float top = stream->get_video_area_top();
|
||||
for (bool dock_right : {false, true}) {
|
||||
if (dock_right) {
|
||||
stream->invoke_begin_control_drag(5, 20);
|
||||
stream->invoke_move_control_drag(1275, 20);
|
||||
stream->invoke_end_control_drag();
|
||||
settle();
|
||||
}
|
||||
const int control_x = dock_right ? 916 : 0;
|
||||
const slint::LogicalPosition button({float(control_x + (dock_right ? 278 : 245)), top + 19});
|
||||
stream->window().dispatch_pointer_move_event(button);
|
||||
settle(35); // Include the tooltip that appears before the click.
|
||||
for (bool expanded : {true, false, true, false}) {
|
||||
stream->window().dispatch_pointer_press_event(button, slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_pointer_release_event(button, slint::PointerEventButton::Left);
|
||||
settle(); // No pointer movement: animation must finish on its own.
|
||||
for (int y : {65, 195}) {
|
||||
const auto pixel = adapter.pixels[(int(top) + y) * 1280 + control_x + 4];
|
||||
const bool painted = pixel.r >= 240 && pixel.g >= 240 && pixel.b >= 240;
|
||||
const bool cleared = pixel.r == 0 && pixel.g == 0 && pixel.b == 0;
|
||||
if (stats_visible != expanded || (expanded ? !painted : !cleared)) {
|
||||
std::cerr << "Stats must fully " << (expanded ? "expand" : "collapse")
|
||||
<< " without pointer movement; dock_right=" << dock_right
|
||||
<< ", y=" << y << ", pixel=" << int(pixel.r) << ','
|
||||
<< int(pixel.g) << ',' << int(pixel.b) << '\n';
|
||||
return 1;
|
||||
}
|
||||
if (stream->invoke_is_local_control_area(control_x + 4, y) != expanded) {
|
||||
std::cerr << "Stats input area must match the rendered panel\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << "Stream stats retained-buffer rendering passed.\n";
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "ui/release_notes_text.h"
|
||||
#include "ui/ui_localization.h"
|
||||
|
||||
#include <slint-platform.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <charconv>
|
||||
@@ -14,6 +16,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -845,9 +848,71 @@ bool VerifyWorkbenchActions() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VerifySystemShortcutMenu() {
|
||||
auto stream = crossdesk::ui::StreamWindow::create();
|
||||
stream->window().set_size(slint::LogicalSize(slint::Size<float>{1280, 720}));
|
||||
std::vector<std::string> commands;
|
||||
stream->on_send_shortcut([&](slint::SharedString command) { commands.emplace_back(command); });
|
||||
const auto click = [&](float y) {
|
||||
const slint::LogicalPosition point({55, stream->get_video_area_top() + y});
|
||||
stream->window().dispatch_pointer_move_event(point);
|
||||
stream->window().dispatch_pointer_press_event(point, slint::PointerEventButton::Left);
|
||||
stream->window().dispatch_pointer_release_event(point, slint::PointerEventButton::Left);
|
||||
};
|
||||
click(20);
|
||||
if (!stream->invoke_is_local_control_area(55, 90)) return false;
|
||||
click(58);
|
||||
stream->set_secure_attention_supported(false);
|
||||
click(20);
|
||||
if (stream->invoke_is_local_control_area(55, 90)) return false;
|
||||
click(58);
|
||||
stream->set_secure_attention_supported(true);
|
||||
click(20); click(88);
|
||||
return commands == std::vector<std::string>{"Ctrl+Alt+Del", "Win+L", "Win+L"};
|
||||
}
|
||||
|
||||
bool VerifyRemoteIdEditing() {
|
||||
auto window = crossdesk::ui::MainWindow::create();
|
||||
window->window().set_size(slint::LogicalSize(slint::Size<float>{
|
||||
window->get_minimum_window_width(), window->get_minimum_window_height()}));
|
||||
window->on_format_remote_id([](slint::SharedString text) { return text; });
|
||||
window->invoke_reset_remote_id();
|
||||
const auto key = [&](const slint::SharedString& text) {
|
||||
window->window().dispatch_key_press_event(text);
|
||||
window->window().dispatch_key_release_event(text);
|
||||
};
|
||||
const auto settle = [] {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
slint::platform::update_timers_and_animations();
|
||||
}
|
||||
};
|
||||
settle(); // Let the initial timer state settle before the first user edit.
|
||||
for (char digit : std::string("1234")) {
|
||||
key(slint::SharedString(std::string(1, digit)));
|
||||
settle(); // Separate edits must not leave the formatter repeating.
|
||||
}
|
||||
if (std::string(window->get_remote_id_input()) != "1234") return false;
|
||||
window->window().dispatch_key_press_event(slint::platform::key_codes::Control);
|
||||
key("a");
|
||||
window->window().dispatch_key_release_event(slint::platform::key_codes::Control);
|
||||
settle(); // Formatting must not repeatedly collapse the selection.
|
||||
key("5");
|
||||
settle();
|
||||
return std::string(window->get_remote_id_input()) == "5";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
if (!VerifySystemShortcutMenu()) {
|
||||
std::cerr << "Shortcut menu must omit unsupported SAS and resize its input area\n";
|
||||
return 13;
|
||||
}
|
||||
if (!VerifyRemoteIdEditing()) {
|
||||
std::cerr << "Remote ID editor must preserve Ctrl+A selection after formatting\n";
|
||||
return 12;
|
||||
}
|
||||
const CaptureOptions capture_options = LoadCaptureOptions();
|
||||
if (!VerifyMainWindowGeometry()) {
|
||||
std::cerr << "Main window dimensions must stay stable through startup\n";
|
||||
|
||||
@@ -230,6 +230,32 @@ function setup_targets()
|
||||
add_files("apps/desktop/tests/slint_ui_smoke_test.cpp")
|
||||
add_rules("crossdesk.slint_runtime")
|
||||
|
||||
target("slint_stream_render_test")
|
||||
set_kind("binary")
|
||||
set_languages("c++20")
|
||||
set_default(false)
|
||||
if is_os("windows") then
|
||||
add_cxxflags("/bigobj")
|
||||
end
|
||||
add_packages("slint")
|
||||
add_deps("crossdesk_ui")
|
||||
add_includedirs("apps/desktop/src/gui")
|
||||
add_files("apps/desktop/tests/slint_stream_render_test.cpp")
|
||||
add_rules("crossdesk.slint_runtime")
|
||||
|
||||
target("slint_popup_render_test")
|
||||
set_kind("binary")
|
||||
set_languages("c++20")
|
||||
set_default(false)
|
||||
if is_os("windows") then
|
||||
add_cxxflags("/bigobj")
|
||||
end
|
||||
add_packages("slint")
|
||||
add_deps("crossdesk_ui")
|
||||
add_includedirs("apps/desktop/src/gui")
|
||||
add_files("apps/desktop/tests/slint_popup_render_test.cpp")
|
||||
add_rules("crossdesk.slint_runtime")
|
||||
|
||||
target("version_checker_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
|
||||
Reference in New Issue
Block a user