diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 2aa9353f9..c62ad671e 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -168,6 +168,29 @@ class ShowRemoteCursorState { static RxBool find(String id) => Get.find(tag: tag(id)); } +class ShowRemoteCursorLockState { + static String tag(String id) => 'show_remote_cursor_lock_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + class KeyboardEnabledState { static String tag(String id) => 'keyboard_enabled_$id'; @@ -315,6 +338,7 @@ initSharedStates(String id) { CurrentDisplayState.init(id); KeyboardEnabledState.init(id); ShowRemoteCursorState.init(id); + ShowRemoteCursorLockState.init(id); RemoteCursorMovedState.init(id); FingerprintState.init(id); PeerBoolOption.init(id, 'zoom-cursor', () => false); @@ -327,6 +351,7 @@ removeSharedStates(String id) { BlockInputState.delete(id); CurrentDisplayState.delete(id); ShowRemoteCursorState.delete(id); + ShowRemoteCursorLockState.delete(id); KeyboardEnabledState.delete(id); RemoteCursorMovedState.delete(id); FingerprintState.delete(id); diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index c6c8f6443..56340ec54 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -212,6 +212,8 @@ List<(String, String)> otherDefaultSettings() { if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar), if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'), ('Show remote cursor', 'show_remote_cursor'), + ('Follow remote cursor', 'follow_remote_cursor'), + ('Follow remote window focus', 'follow_remote_window'), if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'), ('Show quality monitor', 'show_quality_monitor'), ('Mute', 'disable_audio'), diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index e38e3fb75..a37111541 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -371,7 +371,7 @@ Future>> toolbarCodec( ]; } -Future> toolbarDisplayToggle( +Future> toolbarCursor( BuildContext context, String id, FFI ffi) async { List v = []; final ffiModel = ffi.ffiModel; @@ -384,12 +384,17 @@ Future> toolbarDisplayToggle( !ffi.canvasModel.cursorEmbedded && !pi.isWayland) { final state = ShowRemoteCursorState.find(id); + final lockState = ShowRemoteCursorLockState.find(id); final enabled = !ffiModel.viewOnly; final option = 'show-remote-cursor'; + if (pi.currentDisplay == kAllDisplayValue || + bind.sessionIsMultiUiSession(sessionId: sessionId)) { + lockState.value = false; + } v.add(TToggleMenu( child: Text(translate('Show remote cursor')), value: state.value, - onChanged: enabled + onChanged: enabled && !lockState.value ? (value) async { if (value == null) return; await bind.sessionToggleOption( @@ -399,6 +404,67 @@ Future> toolbarDisplayToggle( } : null)); } + // follow remote cursor + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland && + versionCmp(pi.version, "1.2.4") >= 0 && + pi.displays.length > 1 && + pi.currentDisplay != kAllDisplayValue && + !bind.sessionIsMultiUiSession(sessionId: sessionId)) { + final option = 'follow-remote-cursor'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + final showCursorOption = 'show-remote-cursor'; + final showCursorState = ShowRemoteCursorState.find(id); + final showCursorLockState = ShowRemoteCursorLockState.find(id); + final showCursorEnabled = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + showCursorLockState.value = value; + if (value && !showCursorEnabled) { + await bind.sessionToggleOption( + sessionId: sessionId, value: showCursorOption); + showCursorState.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + } + v.add(TToggleMenu( + child: Text(translate('Follow remote cursor')), + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + showCursorLockState.value = value; + if (!showCursorEnabled) { + await bind.sessionToggleOption( + sessionId: sessionId, value: showCursorOption); + showCursorState.value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: showCursorOption); + } + })); + } + // follow remote window focus + if (pi.platform != kPeerPlatformAndroid && + !ffi.canvasModel.cursorEmbedded && + !pi.isWayland && + versionCmp(pi.version, "1.2.4") >= 0 && + pi.displays.length > 1 && + pi.currentDisplay != kAllDisplayValue && + !bind.sessionIsMultiUiSession(sessionId: sessionId)) { + final option = 'follow-remote-window'; + final value = + bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); + v.add(TToggleMenu( + child: Text(translate('Follow remote window focus')), + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(sessionId: sessionId, value: option); + value = bind.sessionGetToggleOptionSync( + sessionId: sessionId, arg: option); + })); + } // zoom cursor final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? ''; if (!isMobile && @@ -417,6 +483,17 @@ Future> toolbarDisplayToggle( }, )); } + return v; +} + +Future> toolbarDisplayToggle( + BuildContext context, String id, FFI ffi) async { + List v = []; + final ffiModel = ffi.ffiModel; + final pi = ffiModel.pi; + final perms = ffiModel.permissions; + final sessionId = ffi.sessionId; + // show quality monitor final option = 'show-quality-monitor'; v.add(TToggleMenu( diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index fd048337c..aa1b346fa 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1045,7 +1045,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { @override Widget build(BuildContext context) { _screenAdjustor.updateScreen(); - menuChildrenGetter() { final menuChildren = [ _screenAdjustor.adjustWindow(context), @@ -1069,6 +1068,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { ffi: widget.ffi, ), Divider(), + cursorToggles(), + Divider(), toggles(), ]; // privacy mode @@ -1212,6 +1213,23 @@ class _DisplayMenuState extends State<_DisplayMenu> { }); } + cursorToggles() { + return futureBuilder( + future: toolbarCursor(context, id, ffi), + hasData: (data) { + final v = data as List; + if (v.isEmpty) return Offstage(); + return Column( + children: v + .map((e) => CkbMenuButton( + value: e.value, + onChanged: e.onChanged, + child: e.child, + ffi: ffi)) + .toList()); + }); + } + toggles() { return futureBuilder( future: toolbarDisplayToggle(context, id, ffi), diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index a6f4fd389..7cdb7471c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -836,6 +836,7 @@ void showOptions( List> imageQualityRadios = await toolbarImageQuality(context, id, gFFI); List> codecRadios = await toolbarCodec(context, id, gFFI); + List cursorToggles = await toolbarCursor(context, id, gFFI); List displayToggles = await toolbarDisplayToggle(context, id, gFFI); @@ -876,8 +877,23 @@ void showOptions( })), if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), ]; + final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList(); + final cursorTogglesList = cursorToggles + .asMap() + .entries + .map((e) => Obx(() => CheckboxListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + value: rxCursorToggleValues[e.key].value, + onChanged: (v) { + e.value.onChanged?.call(v); + if (v != null) rxCursorToggleValues[e.key].value = v; + }, + title: e.value.child))) + .toList(); + final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); - final toggles = displayToggles + final displayTogglesList = displayToggles .asMap() .entries .map((e) => Obx(() => CheckboxListTile( @@ -890,6 +906,11 @@ void showOptions( }, title: e.value.child))) .toList(); + final toggles = [ + ...cursorTogglesList, + if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border), + ...displayTogglesList, + ]; Widget privacyModeWidget = Offstage(); if (privacyModeList.length > 1) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4da7d54cd..65e29b2fa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -367,6 +367,8 @@ class FfiModel with ChangeNotifier { } } else if (name == 'sync_peer_option') { _handleSyncPeerOption(evt, peerId); + } else if (name == 'follow_current_display') { + handleFollowCurrentDisplay(evt, sessionId, peerId); } else { debugPrint('Unknown event name: $name'); } @@ -440,7 +442,7 @@ class FfiModel with ChangeNotifier { } } - updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) { + updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) { final newRect = displaysRect(); if (newRect == null) { return; @@ -1040,9 +1042,30 @@ class FfiModel with ChangeNotifier { json.encode(_pi.platformAdditions); } + handleFollowCurrentDisplay( + Map evt, SessionID sessionId, String peerId) async { + if (evt['display_idx'] != null) { + if (pi.currentDisplay == kAllDisplayValue) { + return; + } + _pi.currentDisplay = int.parse(evt['display_idx']); + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + bind.sessionSwitchDisplay( + isDesktop: isDesktop, + sessionId: sessionId, + value: Int32List.fromList([_pi.currentDisplay]), + ); + } + notifyListeners(); + } + // Directly switch to the new display without waiting for the response. switchToNewDisplay(int display, SessionID sessionId, String peerId, - {bool updateCursorPos = true}) { + {bool updateCursorPos = false}) { // VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays. parent.target?.recordingModel.onClose(); // no need to wait for the response diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 91e7e9711..1ccc266ac 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -346,6 +346,10 @@ class RustdeskImpl { return mode == kKeyLegacyMode; } + bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) { + return false; + } + Future sessionSetCustomImageQuality( {required UuidValue sessionId, required int value, dynamic hint}) { return Future(() => js.context.callMethod('setByName', [ diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 555b1df43..483e12c13 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -603,6 +603,8 @@ message OptionMessage { // Resolution custom_resolution = 13; // BoolOption support_windows_specific_session = 14; // starting from 15 please, do not use removed fields + BoolOption follow_remote_cursor = 15; + BoolOption follow_remote_window = 16; } message TestDelay { @@ -765,6 +767,7 @@ message Misc { uint32 selected_sid = 35; DisplayResolution change_display_resolution = 36; MessageQuery message_query = 37; + int32 follow_current_display = 38; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 4acfb2cdf..1660fbb92 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -281,6 +281,10 @@ pub struct PeerConfig { pub enable_file_transfer: EnableFileTransfer, #[serde(flatten)] pub show_quality_monitor: ShowQualityMonitor, + #[serde(flatten)] + pub follow_remote_cursor: FollowRemoteCursor, + #[serde(flatten)] + pub follow_remote_window: FollowRemoteWindow, #[serde( default, deserialize_with = "deserialize_string", @@ -353,6 +357,8 @@ impl Default for PeerConfig { disable_clipboard: Default::default(), enable_file_transfer: Default::default(), show_quality_monitor: Default::default(), + follow_remote_cursor: Default::default(), + follow_remote_window: Default::default(), keyboard_mode: Default::default(), view_only: Default::default(), reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), @@ -1258,6 +1264,19 @@ serde_field_bool!( default_show_remote_cursor, "ShowRemoteCursor::default_show_remote_cursor" ); +serde_field_bool!( + FollowRemoteCursor, + "follow_remote_cursor", + default_follow_remote_cursor, + "FollowRemoteCursor::default_follow_remote_cursor" +); + +serde_field_bool!( + FollowRemoteWindow, + "follow_remote_window", + default_follow_remote_window, + "FollowRemoteWindow::default_follow_remote_window" +); serde_field_bool!( ShowQualityMonitor, "show_quality_monitor", diff --git a/src/client.rs b/src/client.rs index 5cf16c7ce..c765c02cf 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1456,6 +1456,22 @@ impl LoginConfigHandler { BoolOption::No }) .into(); + } else if name == "follow-remote-cursor" { + config.follow_remote_cursor.v = !config.follow_remote_cursor.v; + option.follow_remote_cursor = (if config.follow_remote_cursor.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); + } else if name == "follow-remote-window" { + config.follow_remote_window.v = !config.follow_remote_window.v; + option.follow_remote_window = (if config.follow_remote_window.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); } else if name == "disable-audio" { config.disable_audio.v = !config.disable_audio.v; option.disable_audio = (if config.disable_audio.v { @@ -1601,6 +1617,12 @@ impl LoginConfigHandler { if view_only || self.get_toggle_option("show-remote-cursor") { msg.show_remote_cursor = BoolOption::Yes.into(); } + if self.get_toggle_option("follow-remote-cursor") { + msg.follow_remote_cursor = BoolOption::Yes.into(); + } + if self.get_toggle_option("follow-remote-window") { + msg.follow_remote_window = BoolOption::Yes.into(); + } if !view_only && self.get_toggle_option("lock-after-session-end") { msg.lock_after_session_end = BoolOption::Yes.into(); } @@ -1692,6 +1714,10 @@ impl LoginConfigHandler { self.config.allow_swap_key.v } else if name == "view-only" { self.config.view_only.v + } else if name == "follow-remote-cursor" { + self.config.follow_remote_cursor.v + } else if name == "follow-remote-window" { + self.config.follow_remote_window.v } else { !self.get_option(name).is_empty() } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index a8fa4b67b..0af3f92d7 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1504,7 +1504,9 @@ impl Remote { log::info!("update supported encoding:{:?}", e); self.handler.lc.write().unwrap().supported_encoding = e; } - + Some(misc::Union::FollowCurrentDisplay(d_idx)) => { + self.handler.set_current_display(d_idx); + } _ => {} }, Some(message::Union::TestDelay(t)) => { diff --git a/src/flutter.rs b/src/flutter.rs index aee4ff67b..ce706b447 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -882,6 +882,21 @@ impl InvokeUiSession for FlutterHandler { ); } + fn is_multi_ui_session(&self) -> bool { + self.session_handlers.read().unwrap().len() > 1 + } + + fn set_current_display(&self, disp_idx: i32) { + if self.is_multi_ui_session() { + return; + } + self.push_event( + "follow_current_display", + &[("display_idx", &disp_idx.to_string())], + &[], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 016263e74..518dd47dd 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -213,6 +213,14 @@ pub fn session_refresh(session_id: SessionID, display: usize) { } } +pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(session.is_multi_ui_session()) + } else { + SyncReturn(false) + } +} + pub fn session_record_screen( session_id: SessionID, start: bool, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 2b3d9faec..626f1bcfb 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 6384083d6..9aa3ddac9 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 8cf9738c4..2ac1fbbae 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 25851db83..26d2f7b49 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -604,5 +604,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "打开 Web 控制台以执行更多操作"), ("allow-only-conn-window-open-tip", "仅当 RustDesk 窗口打开时允许连接"), ("no_need_privacy_mode_no_physical_displays_tip", "没有物理显示器,没必要使用隐私模式。"), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 10c78f118..62ca5b4ac 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Více na webové konzoli"), ("allow-only-conn-window-open-tip", "Povolit připojení pouze v případě, že je otevřené okno RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Žádné fyzické displeje, není třeba používat režim soukromí."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9e28f4b4d..d05c0cd9b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ed97679b2..8c61ba866 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Mehr über Webkonsole"), ("allow-only-conn-window-open-tip", "Verbindung nur zulassen, wenn das RustDesk-Fenster geöffnet ist"), ("no_need_privacy_mode_no_physical_displays_tip", "Keine physischen Bildschirme; keine Notwendigkeit, den Datenschutzmodus zu verwenden."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 8fcb403b9..22953dd74 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 35e4eff53..4026f0cd9 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -222,5 +222,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "More on web console"), ("allow-only-conn-window-open-tip", "Only allow connection if RustDesk window is open"), ("no_need_privacy_mode_no_physical_displays_tip", "No physical displays, no need to use the privacy mode."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0e38c6d9f..44a1d01a0 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 92ebb67e9..a1ca3ff9e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Más en consola web"), ("allow-only-conn-window-open-tip", "Permitir la conexión solo si la ventana RusDesk está abierta"), ("no_need_privacy_mode_no_physical_displays_tip", "No hay pantallas físicas, no es necesario usar el modo privado."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 7bb359cd8..6dad57845 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 8cef3ff08..872f8f5d0 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "اطلاعات بیشتر در کنسول وب"), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 4353b5889..43d21b6d0 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index d9b3d0c54..e5b4429fe 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index def491c52..cf65fe093 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -602,5 +602,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Više na web konzoli"), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 6cd18ce80..5c568c32d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 696e75b81..cca611212 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index bb27633ac..cf08daab9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Altre info sulla console web"), ("allow-only-conn-window-open-tip", "Consenti la connessione solo se la finestra RustDesk è aperta"), ("no_need_privacy_mode_no_physical_displays_tip", "Nessun display fisico, nessuna necessità di usare la modalità privacy."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 6d1164115..7d154e650 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 95c003de6..09a4f8101 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9ef2113cb..8642012df 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 5f21af1c1..8864408a3 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 6e0fd5a84..2afb32aae 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Vairāk par tīmekļa konsoli"), ("allow-only-conn-window-open-tip", "Atļaut savienojumu tikai tad, ja ir atvērts RustDesk logs"), ("no_need_privacy_mode_no_physical_displays_tip", "Nav fizisku displeju, nav jāizmanto privātuma režīms."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index e6d6b3e7b..2bc2f4834 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 23f7b721a..026cc92b8 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Meer over de webconsole"), ("allow-only-conn-window-open-tip", "Alleen verbindingen toestaan als het RustDesk-venster geopend is"), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9f8124b15..7f4d5de84 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Więcej w konsoli web"), ("allow-only-conn-window-open-tip", "Zezwalaj na połączenie tylko wtedy, gdy okno RustDesk jest otwarte"), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d483a7ed3..70baf3248 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index cf71f8a57..c7a0126a8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index abf0e078c..eca66abc0 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ab9fcc0b1..a1e5a7c6b 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Больше в веб-консоли"), ("allow-only-conn-window-open-tip", "Разрешать подключение только при открытом окне RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Физические дисплеи отсутствуют, нет необходимости использовать режим конфиденциальности."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 3c63acdf5..450956ef1 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Viac na webovej konzole"), ("allow-only-conn-window-open-tip", "Povoliť pripojenie iba vtedy, ak je otvorené okno aplikácie RustDesk"), ("no_need_privacy_mode_no_physical_displays_tip", "Žiadne fyzické displeje, nie je potrebné používať režim ochrany osobných údajov."), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6559c0cf1..4c6e4dcb3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1a1f9c291..dba37a298 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 18918f645..c1ddb69e1 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index c2a0a73f3..8bc643257 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 3a2a007db..86dbdd4f1 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 05285579d..558b7ed12 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 0b2de53d5..bf8d03f0c 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index c7ae7e427..5181d3074 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "打開 Web 控制台以進行更多操作"), ("allow-only-conn-window-open-tip", "只在 RustDesk 視窗開啟時允許連接"), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 6d18e7a70..4c668cec1 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", "Детальніше про веб-консоль"), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 117442756..a70679062 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -603,5 +603,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), + ("Follow remote cursor", ""), + ("Follow remote window focus", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 105e96fa8..2bdef3bbd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,9 @@ mod keyboard; /// cbindgen:ignore pub mod platform; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; +pub use platform::{ + get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, start_os_service, +}; #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore mod server; @@ -36,15 +38,15 @@ pub mod flutter; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] pub mod flutter_ffi; use common::*; +mod auth_2fa; #[cfg(feature = "cli")] pub mod cli; #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] pub mod core_main; -mod lang; mod custom_server; +mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -mod auth_2fa; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 3c9b6c57f..aac7033da 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -11,7 +11,7 @@ use hbb_common::{ config::Config, libc::{c_char, c_int, c_long, c_void}, log, - message_proto::Resolution, + message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, }; use std::{ @@ -53,6 +53,20 @@ extern "C" { screen_num: *mut c_int, ) -> c_int; fn xdo_new(display: *const c_char) -> Xdo; + fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int; + fn xdo_get_window_location( + xdo: Xdo, + window: *mut c_void, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, + ) -> c_int; + fn xdo_get_window_size( + xdo: Xdo, + window: *mut c_void, + width: *mut c_int, + height: *mut c_int, + ) -> c_int; } #[link(name = "X11")] @@ -119,6 +133,50 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { pub fn reset_input_cache() {} +pub fn get_focused_display(displays: Vec) -> Option { + let mut res = None; + XDO.with(|xdo| { + if let Ok(xdo) = xdo.try_borrow_mut() { + if xdo.is_null() { + return; + } + let mut x: c_int = 0; + let mut y: c_int = 0; + let mut width: c_int = 0; + let mut height: c_int = 0; + let mut window: *mut c_void = std::ptr::null_mut(); + + unsafe { + if xdo_get_active_window(*xdo, &mut window) != 0 { + return; + } + if xdo_get_window_location( + *xdo, + window, + &mut x as _, + &mut y as _, + std::ptr::null_mut(), + ) != 0 + { + return; + } + if xdo_get_window_size(*xdo, window, &mut width as _, &mut height as _) != 0 { + return; + } + let center_x = x + width / 2; + let center_y = y + height / 2; + res = displays.iter().position(|d| { + center_x >= d.x + && center_x < d.x + d.width + && center_y >= d.y + && center_y < d.y + d.height + }); + } + } + }); + res +} + pub fn get_cursor() -> ResultType> { let mut res = None; DISPLAY.with(|conn| { @@ -1228,7 +1286,7 @@ mod desktop { if !home.is_empty() { assert_eq!(d.home, home); } else { - // + // } } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index e8e91ff2c..92991cdcf 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -17,8 +17,13 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::sysinfo::{Pid, Process, ProcessRefreshKind, System}; -use hbb_common::{anyhow::anyhow, bail, log, message_proto::Resolution}; +use hbb_common::{ + allow_err, + anyhow::anyhow, + bail, log, + message_proto::{DisplayInfo, Resolution}, + sysinfo::{Pid, Process, ProcessRefreshKind, System}, +}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -302,6 +307,20 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { */ } +pub fn get_focused_display(displays: Vec) -> Option { + unsafe { + let main_screen: id = msg_send![class!(NSScreen), mainScreen]; + let screen: id = msg_send![main_screen, deviceDescription]; + let id: id = + msg_send![screen, objectForKey: NSString::alloc(nil).init_str("NSScreenNumber")]; + let display_name: u32 = msg_send![id, unsignedIntValue]; + + displays + .iter() + .position(|d| d.name == display_name.to_string()) + } +} + pub fn get_cursor() -> ResultType> { unsafe { let seed = CGSCurrentCursorSeed(); diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 50a48b153..3072c5623 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -12,7 +12,7 @@ use hbb_common::{ bail, config::{self, Config}, log, - message_proto::{Resolution, WindowsSession}, + message_proto::{DisplayInfo, Resolution, WindowsSession}, sleep, timeout, tokio, }; use std::process::{Command, Stdio}; @@ -65,6 +65,24 @@ use windows_service::{ use winreg::enums::*; use winreg::RegKey; +pub fn get_focused_display(displays: Vec) -> Option { + unsafe { + let hWnd = GetForegroundWindow(); + let mut rect: RECT = mem::zeroed(); + if GetWindowRect(hWnd, &mut rect as *mut RECT) == 0 { + return None; + } + displays.iter().position(|display| { + let center_x = rect.left + (rect.right - rect.left) / 2; + let center_y = rect.top + (rect.bottom - rect.top) / 2; + center_x >= display.x + && center_x <= display.x + display.width + && center_y >= display.y + && center_y <= display.y + display.height + }) + } +} + pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { #[allow(invalid_value)] diff --git a/src/server.rs b/src/server.rs index 9345936e0..d54629345 100644 --- a/src/server.rs +++ b/src/server.rs @@ -50,6 +50,7 @@ pub const NAME: &'static str = ""; pub mod input_service { pub const NAME_CURSOR: &'static str = ""; pub const NAME_POS: &'static str = ""; +pub const NAME_WINDOW_FOCUS: &'static str = ""; } } } @@ -105,6 +106,7 @@ pub fn new() -> ServerPtr { if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); + server.add_service(Box::new(input_service::new_window_focus())); } } Arc::new(RwLock::new(server)) @@ -354,6 +356,15 @@ impl Server { } } + fn get_subbed_displays_count(&self, conn_id: i32) -> usize { + self.services + .keys() + .filter(|k| { + Self::is_video_service_name(k) && self.services.get(*k).unwrap().is_subed(conn_id) + }) + .count() + } + fn capture_displays( &mut self, conn: ConnInner, diff --git a/src/server/connection.rs b/src/server/connection.rs index 9cd9221c9..05a0054d5 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -31,8 +31,7 @@ use hbb_common::platform::linux::run_cmds; use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::Config, - fs, - fs::can_enable_overwrite_detection, + fs::{self, can_enable_overwrite_detection}, futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, @@ -241,6 +240,9 @@ pub struct Connection { delayed_read_dir: Option<(String, bool)>, #[cfg(target_os = "macos")] retina: Retina, + follow_remote_cursor: bool, + follow_remote_window: bool, + multi_ui_session: bool, } impl ConnInner { @@ -348,6 +350,9 @@ impl Connection { network_delay: 0, lock_after_session_end: false, show_remote_cursor: false, + follow_remote_cursor: false, + follow_remote_window: false, + multi_ui_session: false, ip: "".to_owned(), disable_audio: false, #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -666,8 +671,14 @@ impl Connection { #[cfg(target_os = "macos")] conn.retina.set_displays(&_pi.displays); } - #[cfg(target_os = "macos")] Some(message::Union::CursorPosition(pos)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + if conn.follow_remote_cursor { + conn.handle_cursor_switch_display(pos.clone()).await; + } + } + #[cfg(target_os = "macos")] if let Some(new_msg) = conn.retina.on_cursor_pos(&pos, conn.display_idx) { msg = Arc::new(new_msg); } @@ -1308,6 +1319,9 @@ impl Connection { if !self.show_remote_cursor { noperms.push(NAME_POS); } + if !self.follow_remote_window { + noperms.push(NAME_WINDOW_FOCUS); + } if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { noperms.push(super::clipboard_service::NAME); } @@ -2581,6 +2595,14 @@ impl Connection { } else { lock.capture_displays(self.inner.clone(), set, true, true); } + self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; + if self.follow_remote_window { + lock.subscribe( + NAME_WINDOW_FOCUS, + self.inner.clone(), + !self.multi_ui_session, + ); + } drop(lock); } } @@ -2766,6 +2788,24 @@ impl Connection { } } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.follow_remote_cursor.enum_value() { + if q != BoolOption::NotSet { + self.follow_remote_cursor = q == BoolOption::Yes; + } + } + if let Ok(q) = o.follow_remote_window.enum_value() { + if q != BoolOption::NotSet { + self.follow_remote_window = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + NAME_WINDOW_FOCUS, + self.inner.clone(), + self.follow_remote_window, + ); + } + } + } if let Ok(q) = o.disable_audio.enum_value() { if q != BoolOption::NotSet { self.disable_audio = q == BoolOption::Yes; @@ -3126,6 +3166,30 @@ impl Connection { self.inner.send(msg.into()); self.supported_encoding_flag = (true, not_use); } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_cursor_switch_display(&mut self, pos: CursorPosition) { + if self.multi_ui_session { + return; + } + let displays = super::display_service::get_sync_displays(); + let d_index = displays.iter().position(|d| { + let scale = d.scale; + pos.x >= d.x + && pos.y >= d.y + && (pos.x - d.x) as f64 * scale < d.width as f64 + && (pos.y - d.y) as f64 * scale < d.height as f64 + }); + if let Some(d_index) = d_index { + if self.display_idx != d_index { + let mut misc = Misc::new(); + misc.set_follow_current_display(d_index as i32); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(msg_out).await; + } + } + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 0c8263cbd..d3caa1362 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -258,7 +258,6 @@ pub(super) fn get_original_resolution( .into() } -#[cfg(target_os = "linux")] pub(super) fn get_sync_displays() -> Vec { SYNC_DISPLAYS.lock().unwrap().displays.clone() } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 81ca155d7..2b180a7c4 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -28,6 +28,7 @@ use std::{ use winapi::um::winuser::WHEEL_DELTA; const INVALID_CURSOR_POS: i32 = i32::MIN; +const INVALID_DISPLAY_IDX: i32 = -1; #[derive(Default)] struct StateCursor { @@ -74,6 +75,29 @@ impl StatePos { } } +#[derive(Default)] +struct StateWindowFocus { + display_idx: i32, +} + +impl super::service::Reset for StateWindowFocus { + fn reset(&mut self) { + self.display_idx = INVALID_DISPLAY_IDX; + } +} + +impl StateWindowFocus { + #[inline] + fn is_valid(&self) -> bool { + self.display_idx != INVALID_DISPLAY_IDX + } + + #[inline] + fn is_changed(&self, disp_idx: i32) -> bool { + self.is_valid() && self.display_idx != disp_idx + } +} + #[derive(Default, Clone, Copy)] struct Input { conn: i32, @@ -238,6 +262,7 @@ fn should_disable_numlock(evt: &KeyEvent) -> bool { pub const NAME_CURSOR: &'static str = "mouse_cursor"; pub const NAME_POS: &'static str = "mouse_pos"; +pub const NAME_WINDOW_FOCUS: &'static str = "window_focus"; #[derive(Clone)] pub struct MouseCursorService { pub sp: ServiceTmpl, @@ -277,6 +302,12 @@ pub fn new_pos() -> GenericService { svc.sp } +pub fn new_window_focus() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME_WINDOW_FOCUS.to_owned(), false); + GenericService::repeat::(&svc.clone(), 33, run_window_focus); + svc.sp +} + #[inline] fn update_last_cursor_pos(x: i32, y: i32) { let mut lock = LATEST_SYS_CURSOR_POS.lock().unwrap(); @@ -352,6 +383,22 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> Ok(()) } +fn run_window_focus(sp: EmptyExtraFieldService, state: &mut StateWindowFocus) -> ResultType<()> { + let displays = super::display_service::get_sync_displays(); + let disp_idx = crate::get_focused_display(displays); + if let Some(disp_idx) = disp_idx.map(|id| id as i32) { + if state.is_changed(disp_idx) { + let mut misc = Misc::new(); + misc.set_follow_current_display(disp_idx as i32); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + sp.send(msg_out); + } + state.display_idx = disp_idx; + } + Ok(()) +} + #[derive(Copy, Clone, PartialEq, Eq, Hash)] enum KeysDown { RdevKey(RawKey), @@ -424,12 +471,15 @@ struct VirtualInputState { #[cfg(target_os = "macos")] impl VirtualInputState { fn new() -> Option { - VirtualInput::new(CGEventSourceStateID::CombinedSessionState, CGEventTapLocation::Session) - .map(|virtual_input| Self { - virtual_input, - capslock_down: false, - }) - .ok() + VirtualInput::new( + CGEventSourceStateID::CombinedSessionState, + CGEventTapLocation::Session, + ) + .map(|virtual_input| Self { + virtual_input, + capslock_down: false, + }) + .ok() } #[inline] diff --git a/src/ui/header.tis b/src/ui/header.tis index 23315af16..b40e664da 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -194,6 +194,8 @@ class Header: Reactor.Component { : ""}
{!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote cursor')}
  • } + {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {(is_win && pi.platform == "Windows") && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} @@ -479,7 +481,7 @@ function toggleMenuState() { for (var el in $$(menu#keyboard-options>li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } - for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key", "i444"]) { + for (var id in ["show-remote-cursor", "follow-remote-cursor", "follow-remote-window", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key", "i444"]) { var el = self.select('#' + id); if (el) { var value = handler.get_toggle_option(id); @@ -538,6 +540,15 @@ handler.setMultipleWindowsSession = function(sessions) { }); } +handler.setCurrentDisplay = function(v) { + pi.current_display = v; + handler.switch_display(v); + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index c2e01c9f5..93a796f4b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -259,6 +259,10 @@ impl InvokeUiSession for SciterHandler { // Ignore for sciter version. } + fn set_current_display(&self, _disp_idx: i32) { + self.call("setCurrentDisplay", &make_args!(_disp_idx)); + } + fn set_multiple_windows_session(&self, sessions: Vec) { let mut v = Value::array(0); let mut sessions = sessions; diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 61c11feb0..3e5b66049 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -192,6 +192,11 @@ impl Session { self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) } + #[cfg(feature = "flutter")] + pub fn is_multi_ui_session(&self) -> bool { + self.ui_handler.is_multi_ui_session() + } + pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -1378,6 +1383,9 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[cfg(all(feature = "vram", feature = "flutter"))] fn on_texture(&self, display: usize, texture: *mut c_void); fn set_multiple_windows_session(&self, sessions: Vec); + fn set_current_display(&self, disp_idx: i32); + #[cfg(feature = "flutter")] + fn is_multi_ui_session(&self) -> bool; } impl Deref for Session {