From 56ebfdf8e77d4f897ea85491e70c5b0cedb7f3da Mon Sep 17 00:00:00 2001 From: dijunkun Date: Sat, 12 Sep 2026 22:04:20 +0800 Subject: [PATCH] [feat] add dedicated privacy screen controls --- .../src/gui/application/gui_application.cpp | 64 ++++++----- .../assets/localization/localization_data.h | 9 +- .../src/gui/runtime/privacy_control_state.h | 58 ++++++++++ apps/desktop/src/gui/runtime/remote_session.h | 1 - apps/desktop/src/gui/ui/common.slint | 1 + apps/desktop/src/gui/ui/main_window.slint | 13 ++- apps/desktop/src/gui/ui/stream_window.slint | 105 +++++++++++------- apps/desktop/src/gui/ui/ui_localization.h | 1 - 8 files changed, 168 insertions(+), 84 deletions(-) create mode 100644 apps/desktop/src/gui/runtime/privacy_control_state.h diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 4f7f8767..de805235 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -34,6 +34,7 @@ #include "platform.h" #include "platform/video_renderer.h" #include "rendering/slint_video_presenter.h" +#include "runtime/privacy_control_state.h" #if _WIN32 #include @@ -1230,13 +1231,15 @@ void GuiApplication::ResetSettingsUi() { ui_->main->set_self_hosted_enabled(enable_self_hosted_); ui_->main->set_autostart_enabled(enable_autostart_); ui_->main->set_daemon_enabled(enable_daemon_); + ui_->main->set_privacy_on_connect_enabled( + config_center_->IsEnablePrivacyScreen()); #ifdef _WIN32 ui_->main->set_capture_method_visible(true); ui_->main->set_capture_method_index( static_cast(config_center_->GetScreenCaptureMethod())); - ui_->main->set_privacy_setting_visible(true); - ui_->main->set_privacy_on_connect_enabled( - config_center_->IsEnablePrivacyScreen()); + ui_->main->set_privacy_setting_available(true); +#else + ui_->main->set_privacy_setting_available(false); #endif ui_->main->set_file_save_path(file_transfer_save_path_buf_); ui_->main->set_server_host(signal_server_ip_self_); @@ -1607,11 +1610,16 @@ void GuiApplication::BindStreamCallbacks() { { std::lock_guard lock(props->privacy_status_mutex_); const auto& status = props->privacy_status_; - action.pc.flag = status.overlay_active || status.remote_paused || status.state == PrivacyState::on || - status.state == PrivacyState::failed ? PrivacyCommandFlag::disable : PrivacyCommandFlag::enable; - action.pc.block_local_input = props->privacy_block_local_input_; + const uint64_t now = SDL_GetTicks(); + const auto controls = GetPrivacyControlState( + status, props->privacy_status_received_, props->privacy_command_pending_, + now - props->privacy_status_tick_, now - props->privacy_command_tick_); + if (!controls.can_toggle) return; + action.pc.flag = controls.active ? PrivacyCommandFlag::disable + : PrivacyCommandFlag::enable; + action.pc.block_local_input = true; props->privacy_command_pending_ = true; - props->privacy_command_tick_ = SDL_GetTicks(); + props->privacy_command_tick_ = now; props->privacy_request_revision_ = status.revision; } const auto message = action.to_json(); @@ -1623,12 +1631,6 @@ void GuiApplication::BindStreamCallbacks() { LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_); } }); - stream->on_toggle_privacy_input([this] { - if (auto props = SelectedSession()) { - std::lock_guard lock(props->privacy_status_mutex_); - props->privacy_block_local_input_ = !props->privacy_block_local_input_; - } - }); stream->on_toggle_mouse_control([this] { auto props = SelectedSession(); if (!props || !props->connection_established_) { @@ -2554,23 +2556,27 @@ void GuiApplication::SyncStreamWindow() { std::lock_guard lock(props->privacy_status_mutex_); const auto& status = props->privacy_status_; const uint64_t now = SDL_GetTicks(); - const bool timeout = props->privacy_command_pending_ && now - props->privacy_command_tick_ > 15000; - const bool stale = props->privacy_status_received_ && now - props->privacy_status_tick_ > 5000; - const bool pending = props->privacy_command_pending_ && !timeout; - const bool active = status.overlay_active || status.remote_paused || status.state == PrivacyState::on || status.state == PrivacyState::failed; - (*ui_->stream)->set_privacy_active(active); - (*ui_->stream)->set_privacy_can_toggle(props->control_mouse_ && - props->connection_status_.load() == ConnectionStatus::Connected && !pending && - (active || (props->privacy_status_received_ && status.supported && !stale))); - (*ui_->stream)->set_privacy_can_change_input(props->control_mouse_ && !active && !pending && status.input_block_supported); - (*ui_->stream)->set_privacy_block_input(active ? status.input_blocked : props->privacy_block_local_input_); - (*ui_->stream)->set_privacy_remote_paused(status.remote_paused || (active && (stale || timeout))); + const auto controls = GetPrivacyControlState( + status, props->privacy_status_received_, props->privacy_command_pending_, + now - props->privacy_status_tick_, now - props->privacy_command_tick_); + const bool connected = + props->connection_status_.load() == ConnectionStatus::Connected; + (*ui_->stream)->set_privacy_active(controls.active); + (*ui_->stream)->set_privacy_off(connected && controls.off); + (*ui_->stream)->set_privacy_unsupported(controls.unsupported); + (*ui_->stream)->set_privacy_can_toggle( + props->control_mouse_ && connected && controls.can_toggle); const int language = localization_language_index_; - std::string text = !props->privacy_status_received_ ? localization::privacy_unknown[language] : - (timeout || stale) ? localization::privacy_timeout[language] : - pending ? localization::privacy_pending[language] : - status.remote_paused ? localization::privacy_paused[language] : status.reason; - if (status.remote_paused && !pending) text += std::string("\n") + status.reason; + std::string text = !connected || !props->privacy_status_received_ + ? localization::privacy_screen[language] + : controls.timeout || controls.stale ? localization::privacy_timeout[language] + : controls.warning ? localization::privacy_paused[language] + : controls.enabled ? localization::privacy_on[language] + : !status.supported ? localization::privacy_unsupported[language] + : controls.off ? localization::privacy_off[language] + : localization::privacy_screen[language]; + if (connected && controls.warning && status.reason[0] != '\0') + text += std::string("\n") + status.reason; (*ui_->stream)->set_privacy_status_text(UiText(text)); } int remote_cursor_shape = diff --git a/apps/desktop/src/gui/assets/localization/localization_data.h b/apps/desktop/src/gui/assets/localization/localization_data.h index ec206d86..392865af 100644 --- a/apps/desktop/src/gui/assets/localization/localization_data.h +++ b/apps/desktop/src/gui/assets/localization/localization_data.h @@ -20,13 +20,14 @@ struct TranslationRow { // Single source of truth for all UI strings. #define CROSSDESK_LOCALIZATION_ALL(X) \ + X(privacy_screen, u8"隐私屏", "Privacy screen", u8"Приватный экран") \ X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \ X(privacy_disable, u8"关闭隐私屏 / 恢复操作", "Turn privacy off / recover", u8"Выключить / восстановить") \ - X(privacy_block_input, u8"开启时屏蔽本地输入", "Block local input on enable", u8"Блокировать локальный ввод") \ - X(privacy_unknown, u8"隐私屏:未收到被控端确认(旧版本可能不支持)", "Privacy: no host confirmation (older hosts may not support it)", u8"Приватность: нет подтверждения от узла") \ - X(privacy_pending, u8"隐私屏:等待被控端完成验证", "Privacy: waiting for host verification", u8"Приватность: ожидание проверки") \ + X(privacy_on, u8"隐私屏:已开启", "Privacy screen: on", u8"Приватный экран: включён") \ + X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \ + X(privacy_unsupported, u8"隐私屏:被控端暂不支持", "Privacy screen: unavailable on this host", u8"Приватный экран: недоступен на узле") \ X(privacy_timeout, u8"隐私屏状态未知:被控端确认超时,可关闭后重试", "Privacy state unknown: host confirmation timed out; turn off to recover", u8"Статус неизвестен: время ожидания истекло") \ - X(privacy_paused, u8"隐私保护未就绪,远程操作已暂停。可在快捷菜单关闭隐私屏恢复。", "Privacy protection is not ready; remote operation paused. Turn privacy off in the shortcut menu to recover.", u8"Приватность не готова; удалённое управление приостановлено.") \ + X(privacy_paused, u8"隐私保护未就绪,远程操作已暂停。点击隐私屏按钮关闭并恢复。", "Privacy protection is not ready; remote operation paused. Click the privacy screen button to turn it off and recover.", u8"Приватность не готова; нажмите кнопку приватного экрана для выключения и восстановления управления.") \ X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \ X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \ X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ diff --git a/apps/desktop/src/gui/runtime/privacy_control_state.h b/apps/desktop/src/gui/runtime/privacy_control_state.h new file mode 100644 index 00000000..6b21d6cc --- /dev/null +++ b/apps/desktop/src/gui/runtime/privacy_control_state.h @@ -0,0 +1,58 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-12 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _PRIVACY_CONTROL_STATE_H_ +#define _PRIVACY_CONTROL_STATE_H_ + +#include + +#include "remote_action.h" + +namespace crossdesk { + +struct PrivacyControlState { + bool active = false; + bool enabled = false; + bool off = false; + bool unsupported = false; + bool warning = false; + bool stale = false; + bool timeout = false; + bool can_toggle = false; +}; + +// Use the same host-confirmed state for presentation and command admission. +// An engaged or failed screen can still be disabled when its status goes stale. +inline PrivacyControlState GetPrivacyControlState( + const PrivacyStatus& status, bool received, bool command_pending, + uint64_t status_age_ms, uint64_t command_age_ms) { + PrivacyControlState result; + result.timeout = command_pending && command_age_ms > 15000; + result.stale = received && status_age_ms > 5000; + const bool request_pending = command_pending && !result.timeout; + const bool fresh = received && !result.stale; + result.active = status.overlay_active || status.remote_paused || + status.state == PrivacyState::on || + status.state == PrivacyState::starting || + status.state == PrivacyState::stopping || + status.state == PrivacyState::failed; + // A command in flight only gates repeated clicks; it adds no display state. + result.enabled = fresh && !result.timeout && + status.state == PrivacyState::on && status.overlay_active && + !status.remote_paused; + result.off = fresh && !result.timeout && + status.state == PrivacyState::off && !result.active; + result.unsupported = received && !status.supported; + result.warning = result.timeout || result.stale || + (received && status.state == PrivacyState::failed); + result.can_toggle = !request_pending && + (result.active || (fresh && status.supported)); + return result; +} + +} // namespace crossdesk + +#endif diff --git a/apps/desktop/src/gui/runtime/remote_session.h b/apps/desktop/src/gui/runtime/remote_session.h index a8d6242a..4e33e891 100644 --- a/apps/desktop/src/gui/runtime/remote_session.h +++ b/apps/desktop/src/gui/runtime/remote_session.h @@ -161,7 +161,6 @@ struct RemoteSession { PrivacyStatus privacy_status_{}; bool privacy_status_received_ = false; bool privacy_command_pending_ = false; - bool privacy_block_local_input_ = true; uint64_t privacy_command_tick_ = 0; uint32_t privacy_request_revision_ = 0; uint64_t privacy_status_tick_ = 0; diff --git a/apps/desktop/src/gui/ui/common.slint b/apps/desktop/src/gui/ui/common.slint index 876acf18..eabb71f9 100644 --- a/apps/desktop/src/gui/ui/common.slint +++ b/apps/desktop/src/gui/ui/common.slint @@ -45,6 +45,7 @@ export global FontAwesomeIcons { out property shield-halved: "\u{f3ed}"; out property square-full: "\u{f45c}"; out property trash-can: "\u{f2ed}"; + out property user-secret: "\u{f21b}"; out property volume-high: "\u{f028}"; out property volume-xmark: "\u{f6a9}"; out property xmark: "\u{f00d}"; diff --git a/apps/desktop/src/gui/ui/main_window.slint b/apps/desktop/src/gui/ui/main_window.slint index 4942dced..7d9255f1 100644 --- a/apps/desktop/src/gui/ui/main_window.slint +++ b/apps/desktop/src/gui/ui/main_window.slint @@ -251,7 +251,8 @@ export component MainWindow inherits Window { in-out property self-hosted-enabled: false; in-out property autostart-enabled: false; in-out property daemon-enabled: false; - in-out property privacy-setting-visible: false; + // Show the preference on every platform; only adapted hosts can change it. + in property privacy-setting-available: false; in-out property privacy-on-connect-enabled: true; in-out property file-save-path: ""; in-out property server-host: ""; @@ -1457,11 +1458,6 @@ export component MainWindow inherits Window { Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; } } - if root.privacy-setting-visible: Rectangle { - height: 30px; - Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } - ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; } - } Rectangle { height: 30px; Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } @@ -1497,6 +1493,11 @@ export component MainWindow inherits Window { Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; } } + Rectangle { + height: 30px; + Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; color: root.privacy-setting-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; } + ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; } + } Rectangle { height: 30px; CompactButton { x: 8px; y: 3px; width: root.compact-language ? 70px : 206px; text: UiStrings.self-hosted; enabled: !root.settings-session-active; clicked => { root.self-host-settings-open = true; } } diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index b7d3f494..eb380727 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -27,7 +27,6 @@ export global StreamStrings { in-out property send-shortcut: "Send Shortcut"; in-out property privacy-enable: "Enable privacy screen"; in-out property privacy-disable: "Turn privacy off / recover"; - in-out property privacy-block-input: "Block local input on enable"; in-out property control-mouse: "Control Mouse"; in-out property release-mouse: "Release Mouse"; in-out property audio: "Audio Capture"; @@ -57,19 +56,23 @@ component ControlBarButton inherits Rectangle { in property tooltip: ""; in property selected: false; in property slashed: false; + in property action-enabled: true; + // Capability and temporary command availability have different visuals. + in property muted: false; in property icon-size: 16px; + out property has-hover: touch.has-hover; callback clicked; width: 24px; height: 24px; - background: touch.pressed ? #0f87fa + background: root.muted ? #eef0f3 : touch.pressed && root.action-enabled ? #0f87fa : touch.has-hover ? #4296fa : root.selected ? #4296fa : #4296fa66; border-radius: 3px; Text { text: root.icon; - color: #24272d; + color: root.muted ? #8a8f96 : #24272d; font-family: "Font Awesome 6 Free"; font-weight: 900; font-size: root.icon-size; @@ -108,7 +111,8 @@ component ControlBarButton inherits Rectangle { } touch := TouchArea { - clicked => { root.clicked(); } + // Keep disabled controls hit-testable so clicks cannot reach the peer. + clicked => { if root.action-enabled { root.clicked(); } } } } @@ -139,10 +143,10 @@ export component StreamWindow inherits Window { in property mouse-control-enabled: true; in property privacy-can-toggle: false; in property privacy-active: false; - in property privacy-block-input: true; - in property privacy-can-change-input: false; - in property privacy-remote-paused: false; - in property privacy-status-text: "Privacy screen: waiting for host"; + in property privacy-off: false; + in property privacy-unsupported: false; + // Privacy feedback stays on its toolbar button and hover tooltip. + in property privacy-status-text: "Privacy screen"; // Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor. in property remote-cursor-active: false; in property remote-cursor-shape: 0; @@ -164,7 +168,6 @@ export component StreamWindow inherits Window { callback send-shortcut(string); callback toggle-mouse-control; callback toggle-privacy; - callback toggle-privacy-input; callback toggle-audio; callback select-file; callback close-file-transfer; @@ -187,6 +190,13 @@ export component StreamWindow inherits Window { private property display-menu-animation-enabled: false; private property display-menu-progress: 0.0; private property shortcut-menu-open: false; + private property privacy-tooltip-visible: false; + private property privacy-hovered: root.control-expanded + && privacy-button.has-hover && !root.control-dragging; + private property privacy-tooltip-width: min(320px, video.width); + private property privacy-tooltip-x: min(max(0px, + control.x + (root.control-docked-left ? 105px : 138px)), + video.width - root.privacy-tooltip-width); // A popup can remove itself from the scene in its clicked callback before // the video input layer observes the same pointer release. Remember that // the press belonged to local UI so that release cannot reach the peer. @@ -226,6 +236,13 @@ export component StreamWindow inherits Window { } } + changed privacy-hovered => { root.privacy-tooltip-visible = false; } + privacy-tooltip-timer := Timer { + interval: 450ms; + running: root.privacy-hovered && !root.privacy-tooltip-visible; + triggered => { root.privacy-tooltip-visible = true; } + } + is-local-control-area(pointer-x, pointer-y) => { let over-control = pointer-x >= control.x && pointer-x <= control.x + control.width @@ -243,9 +260,9 @@ export component StreamWindow inherits Window { let shortcut-menu-y = control.y + 40px; let over-shortcut-menu = root.shortcut-menu-open && pointer-x >= shortcut-menu-x - && pointer-x <= shortcut-menu-x + 360px + && pointer-x <= shortcut-menu-x + 180px && pointer-y >= shortcut-menu-y - && pointer-y <= shortcut-menu-y + 200px; + && pointer-y <= shortcut-menu-y + 68px; return over-control || over-display-menu || over-shortcut-menu; } @@ -650,7 +667,7 @@ export component StreamWindow inherits Window { control := Rectangle { x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width; y: min(max(0px, root.control-pos-y), parent.height - self.height); - width: root.control-expanded ? min(300px, parent.width) : 20px; + width: root.control-expanded ? min(332px, parent.width) : 20px; height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px; background: white; border-width: 1px; @@ -702,40 +719,40 @@ export component StreamWindow inherits Window { clicked => { root.toggle-mouse-control(); } } if root.control-expanded: audio-button := ControlBarButton { - x: root.control-docked-left ? 105px : 138px; y: 7px; + x: root.control-docked-left ? 137px : 170px; y: 7px; icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled; tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio; clicked => { root.toggle-audio(); } } if root.control-expanded: file-button := ControlBarButton { - x: root.control-docked-left ? 137px : 170px; y: 7px; + x: root.control-docked-left ? 169px : 202px; y: 7px; icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file; clicked => { root.select-file(); } } if root.control-expanded: stats-button := ControlBarButton { - x: root.control-docked-left ? 169px : 202px; y: 7px; + x: root.control-docked-left ? 201px : 234px; y: 7px; icon: FontAwesomeIcons.signal; selected: root.stats-visible; tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats; clicked => { root.toggle-stats(); } } if root.control-expanded: fullscreen-button := ControlBarButton { - x: root.control-docked-left ? 201px : 234px; y: 7px; + x: root.control-docked-left ? 233px : 266px; y: 7px; icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand; tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen; clicked => { root.toggle-fullscreen(); } } if root.control-expanded: close-button := ControlBarButton { - x: root.control-docked-left ? 233px : 266px; y: 7px; + x: root.control-docked-left ? 265px : 298px; y: 7px; icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect; clicked => { root.disconnect(); } } if root.control-expanded && root.control-docked-left: Rectangle { - x: 267px; y: 12px; width: 2px; height: 15px; + x: 299px; y: 12px; width: 2px; height: 15px; background: ImGuiLineStyle.control-separator; } collapse-button := ControlBarButton { - x: root.control-expanded ? (root.control-docked-left ? 275px : 10px) : 2px; + x: root.control-expanded ? (root.control-docked-left ? 307px : 10px) : 2px; y: 7px; width: 15px; icon: root.control-expanded @@ -745,6 +762,17 @@ export component StreamWindow inherits Window { clicked => { root.control-expanded = !root.control-expanded; } } + privacy-button := ControlBarButton { + visible: root.control-expanded; + x: root.control-docked-left ? 105px : 138px; y: 7px; + icon: FontAwesomeIcons.user-secret; + slashed: root.privacy-off && !root.privacy-unsupported; + muted: root.privacy-unsupported; + action-enabled: root.privacy-can-toggle; + tooltip: root.privacy-status-text; + clicked => { root.toggle-privacy(); } + } + if root.stats-visible && root.control-expanded: stats-table := Rectangle { x: 14px; y: 38px; @@ -954,7 +982,7 @@ export component StreamWindow inherits Window { } if root.shortcut-menu-open: Rectangle { - x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 360px; height: 200px; + x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 180px; height: 68px; background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11; VerticalLayout { padding: 4px; spacing: 1px; @@ -970,32 +998,23 @@ export component StreamWindow inherits Window { clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; } } } - Rectangle { - height: 29px; background: privacy-touch.has-hover ? #e8eef9 : transparent; - Text { x: 8px; text: root.privacy-active ? StreamStrings.privacy-disable : StreamStrings.privacy-enable; color: root.privacy-can-toggle ? #30343b : #8a8f96; font-size: ImGuiFontStyle.body; vertical-alignment: center; } - privacy-touch := TouchArea { - enabled: root.privacy-can-toggle; - pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } } - clicked => { root.toggle-privacy(); } - } - } - Rectangle { - height: 29px; background: privacy-input-touch.has-hover ? #e8eef9 : transparent; - Text { x: 8px; text: (root.privacy-block-input ? "[✓] " : "[ ] ") + StreamStrings.privacy-block-input; color: root.privacy-can-change-input ? #30343b : #8a8f96; font-size: ImGuiFontStyle.body; vertical-alignment: center; } - privacy-input-touch := TouchArea { - enabled: root.privacy-can-change-input; - pointer-event(event) => { if event.kind == PointerEventKind.down { root.suppress-next-remote-pointer-release = true; } } - clicked => { root.toggle-privacy-input(); } - } - } - Text { height: 68px; text: root.privacy-status-text; wrap: word-wrap; color: #555b66; font-size: ImGuiFontStyle.body; } } } - if root.privacy-remote-paused: Rectangle { - x: 0px; y: 65px; width: parent.width; height: parent.height - 65px; - background: #16181d; z: 6; - Text { x: 30px; width: parent.width - 60px; text: root.privacy-status-text; color: #eeeeee; wrap: word-wrap; horizontal-alignment: center; vertical-alignment: center; } + if root.privacy-hovered && root.privacy-tooltip-visible: Rectangle { + x: root.privacy-tooltip-x; + width: root.privacy-tooltip-width; + height: privacy-tip.preferred-height + 16px; + y: control.y + control.height + self.height <= video.height + ? control.y + control.height : max(0px, control.y - self.height); + background: #30343bf5; border-radius: 5px; z: 12; + privacy-tip := Text { + x: 8px; y: 8px; width: parent.width - 16px; + text: root.privacy-status-text + + (root.privacy-can-toggle ? "\n" + (root.privacy-active + ? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : ""); + color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap; + } } // Transfer progress overlay at bottom-left. diff --git a/apps/desktop/src/gui/ui/ui_localization.h b/apps/desktop/src/gui/ui/ui_localization.h index 1e2ecab0..c848def4 100644 --- a/apps/desktop/src/gui/ui/ui_localization.h +++ b/apps/desktop/src/gui/ui/ui_localization.h @@ -144,7 +144,6 @@ inline int ApplyStreamWindowStrings( strings.set_send_shortcut(Text(localization::send_shortcut[language])); strings.set_privacy_enable(Text(localization::privacy_enable[language])); strings.set_privacy_disable(Text(localization::privacy_disable[language])); - strings.set_privacy_block_input(Text(localization::privacy_block_input[language])); strings.set_control_mouse(Text(localization::control_mouse[language])); strings.set_release_mouse(Text(localization::release_mouse[language])); strings.set_audio(Text(localization::audio_capture[language]));