From 013d307bcddf08a0664593c1fca505344c179802 Mon Sep 17 00:00:00 2001 From: dignow Date: Sun, 8 Oct 2023 21:44:54 +0800 Subject: [PATCH 01/10] feat, multi_flutter_ui_sessions Signed-off-by: dignow --- flutter/lib/common.dart | 26 +- flutter/lib/common/widgets/dialog.dart | 2 +- flutter/lib/common/widgets/toolbar.dart | 19 +- flutter/lib/consts.dart | 10 + .../lib/desktop/pages/desktop_home_page.dart | 55 +- .../desktop/pages/desktop_setting_page.dart | 25 + flutter/lib/desktop/pages/remote_page.dart | 214 ++++--- .../lib/desktop/pages/remote_tab_page.dart | 29 +- .../lib/desktop/widgets/remote_toolbar.dart | 316 +++++----- .../lib/desktop/widgets/tabbar_widget.dart | 14 + flutter/lib/mobile/pages/remote_page.dart | 5 +- .../lib/models/desktop_render_texture.dart | 13 +- flutter/lib/models/input_model.dart | 41 +- flutter/lib/models/model.dart | 409 ++++++++++--- flutter/lib/models/native_model.dart | 20 +- flutter/lib/models/state_model.dart | 1 - flutter/lib/utils/multi_window_manager.dart | 59 +- libs/clipboard/src/lib.rs | 19 +- libs/hbb_common/protos/message.proto | 9 + libs/scrap/src/common/aom.rs | 12 +- libs/scrap/src/common/codec.rs | 6 +- libs/scrap/src/common/hwcodec.rs | 12 +- libs/scrap/src/common/vpxcodec.rs | 15 +- src/client.rs | 190 ++++-- src/client/helper.rs | 3 +- src/client/io_loop.rs | 280 +++++---- src/common.rs | 20 +- src/flutter.rs | 579 +++++++++++++----- src/flutter_ffi.rs | 242 +++++--- src/lang/ar.rs | 7 + src/lang/ca.rs | 7 + src/lang/cn.rs | 7 + src/lang/cs.rs | 7 + src/lang/da.rs | 7 + src/lang/de.rs | 9 +- src/lang/el.rs | 7 + src/lang/en.rs | 3 + src/lang/eo.rs | 7 + src/lang/es.rs | 7 + src/lang/fa.rs | 7 + src/lang/fr.rs | 7 + src/lang/hu.rs | 7 + src/lang/id.rs | 7 + src/lang/it.rs | 7 + src/lang/ja.rs | 7 + src/lang/ko.rs | 7 + src/lang/kz.rs | 7 + src/lang/lt.rs | 7 + src/lang/lv.rs | 7 + src/lang/nl.rs | 7 + src/lang/pl.rs | 7 + src/lang/pt_PT.rs | 7 + src/lang/ptbr.rs | 7 + src/lang/ro.rs | 7 + src/lang/ru.rs | 7 + src/lang/sk.rs | 7 + src/lang/sl.rs | 7 + src/lang/sq.rs | 7 + src/lang/sr.rs | 7 + src/lang/sv.rs | 7 + src/lang/template.rs | 7 + src/lang/th.rs | 7 + src/lang/tr.rs | 7 + src/lang/tw.rs | 7 + src/lang/ua.rs | 7 + src/lang/vn.rs | 7 + src/server.rs | 72 ++- src/server/audio_service.rs | 16 +- src/server/clipboard_service.rs | 8 +- src/server/connection.rs | 215 +++++-- src/server/display_service.rs | 262 ++++++++ src/server/input_service.rs | 46 +- src/server/portable_service.rs | 30 +- src/server/service.rs | 88 ++- src/server/video_service.rs | 496 ++++++--------- src/server/wayland.rs | 20 +- src/ui.rs | 3 - src/ui/header.tis | 2 +- src/ui/remote.rs | 14 +- src/ui/remote.tis | 4 +- src/ui_cm_interface.rs | 1 + src/ui_interface.rs | 20 +- src/ui_session_interface.rs | 74 ++- 83 files changed, 2954 insertions(+), 1319 deletions(-) create mode 100644 src/server/display_service.rs diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a3ffcfd35..61d006b8a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1054,7 +1054,7 @@ Widget msgboxIcon(String type) { if (type == 'on-uac' || type == 'on-foreground-elevated') { iconData = Icons.admin_panel_settings; } - if (type == "info") { + if (type.contains('info')) { iconData = Icons.info; } if (iconData != null) { @@ -2317,7 +2317,7 @@ Widget dialogButton(String text, } } -int version_cmp(String v1, String v2) { +int versionCmp(String v1, String v2) { return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); } @@ -2589,3 +2589,25 @@ String getDesktopTabLabel(String peerId, String alias) { } return label; } + +String getChooseDisplayBehavior() { + var current = bind.mainGetOptionSync(key: kKeyChooseDisplayBehavior); + if (![kChooseDisplayBehaviorSwitch, kChooseDisplayBehaviorOpen] + .contains(current)) { + current = kChooseDisplayBehaviorOpen; + } + return current; +} + +sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async { + if (pi.currentDisplay == kAllDisplayValue) { + for (int i = 0; i < pi.displays.length; i++) { + await bind.sessionRefresh(sessionId: sessionId, display: i); + } + } else { + await bind.sessionRefresh(sessionId: sessionId, display: pi.currentDisplay); + } +} + +bool get isChooseDisplayToOpen => + getChooseDisplayBehavior() != kChooseDisplayBehaviorSwitch; diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 2fe5d7637..d1e14d2fb 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1295,7 +1295,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect; } catch (_) {} bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) || - version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0; + versionCmp(ffi.ffiModel.pi.version, '1.2.0') < 0; final content = customImageQualityWidget( initQuality: qualityInitValue, diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 0e8ed6b3d..1717e99f2 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -90,7 +90,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Row(children: [ - Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')), + Text(translate(pi.isHeadless ? 'OS Account' : 'OS Password')), Offstage( offstage: isDesktop, child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12), @@ -99,13 +99,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { trailingIcon: Transform.scale( scale: 0.8, child: InkWell( - onTap: () => pi.is_headless + onTap: () => pi.isHeadless ? showSetOSAccount(sessionId, ffi.dialogManager) : handleOsPasswordEditIcon(sessionId, ffi.dialogManager), child: Icon(Icons.edit), ), ), - onPressed: () => pi.is_headless + onPressed: () => pi.isHeadless ? showSetOSAccount(sessionId, ffi.dialogManager) : handleOsPasswordAction(sessionId, ffi.dialogManager), ), @@ -208,7 +208,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ffiModel.keyboard && pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformMacOS && - version_cmp(pi.version, '1.2.0') >= 0) { + versionCmp(pi.version, '1.2.0') >= 0 && + bind.peerGetDefaultSessionsCount(id: id) == 1) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => @@ -217,8 +218,9 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { // refresh if (pi.version.isNotEmpty) { v.add(TTextMenu( - child: Text(translate('Refresh')), - onPressed: () => bind.sessionRefresh(sessionId: sessionId))); + child: Text(translate('Refresh')), + onPressed: () => sessionRefreshVideo(sessionId, pi), + )); } // record var codecFormat = ffi.qualityMonitorModel.data.codecFormat; @@ -377,7 +379,7 @@ Future> toolbarDisplayToggle( // show remote cursor if (pi.platform != kPeerPlatformAndroid && !ffi.canvasModel.cursorEmbedded && - !pi.is_wayland) { + !pi.isWayland) { final state = ShowRemoteCursorState.find(id); final enabled = !ffiModel.viewOnly; final option = 'show-remote-cursor'; @@ -488,7 +490,8 @@ Future> toolbarDisplayToggle( value: rxValue.value, onChanged: (value) { if (value == null) return; - if (ffiModel.pi.currentDisplay != 0) { + if (ffiModel.pi.currentDisplay != 0 && + ffiModel.pi.currentDisplay != kAllDisplayValue) { msgBox(sessionId, 'custom-nook-nocancel-hasclose', 'info', 'Please switch to Display 1 first', '', ffi.dialogManager); return; diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index e0fffb27d..9c7fdf01d 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,10 +4,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; +const bool kOpenSamePeerInNewWindow = true; + const double kDesktopRemoteTabBarHeight = 28.0; const int kInvalidWindowId = -1; const int kMainWindowId = 0; +const kAllDisplayValue = -1; + const String kPeerPlatformWindows = "Windows"; const String kPeerPlatformLinux = "Linux"; const String kPeerPlatformMacOS = "Mac OS"; @@ -37,11 +41,13 @@ const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewFileTransfer = "new_file_transfer"; const String kWindowEventNewPortForward = "new_port_forward"; const String kWindowEventActiveSession = "active_session"; +const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventGetRemoteList = "get_remote_list"; const String kWindowEventGetSessionIdList = "get_session_id_list"; const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window"; const String kWindowEventGetCachedSessionData = "get_cached_session_data"; +const String kWindowEventOpenMonitorSession = "open_monitor_session"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionOpenInTabs = "allow-open-in-tabs"; @@ -60,6 +66,10 @@ const int kWindowMainId = 0; const String kPointerEventKindTouch = "touch"; const String kPointerEventKindMouse = "mouse"; +const String kKeyChooseDisplayBehavior = 'choose-display-behavior'; +const String kChooseDisplayBehaviorSwitch = 'switch'; +const String kChooseDisplayBehaviorOpen = 'open'; + // the executable name of the portable version const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index c5d3718eb..8ea872c6c 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -187,12 +187,12 @@ class _DesktopHomePageState extends State ? Theme.of(context).scaffoldBackgroundColor : Theme.of(context).colorScheme.background, child: Tooltip( - message: translate('Settings'), - child: Icon( - Icons.more_vert_outlined, - size: 20, - color: hover.value ? textColor : textColor?.withOpacity(0.5), - )), + message: translate('Settings'), + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value ? textColor : textColor?.withOpacity(0.5), + )), ), ), onHover: (value) => hover.value = value, @@ -256,27 +256,27 @@ class _DesktopHomePageState extends State child: Obx(() => RotatedBox( quarterTurns: 2, child: Tooltip( - message: translate('Refresh Password'), - child: Icon( - Icons.refresh, - color: refreshHover.value - ? textColor - : Color(0xFFDDDDDD), - size: 22, - )) - )), + message: translate('Refresh Password'), + child: Icon( + Icons.refresh, + color: refreshHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + )))), onHover: (value) => refreshHover.value = value, ).marginOnly(right: 8, top: 4), InkWell( child: Obx( () => Tooltip( - message: translate('Change Password'), - child: Icon( - Icons.edit, - color: - editHover.value ? textColor : Color(0xFFDDDDDD), - size: 22, - )).marginOnly(right: 8, top: 4), + message: translate('Change Password'), + child: Icon( + Icons.edit, + color: editHover.value + ? textColor + : Color(0xFFDDDDDD), + size: 22, + )).marginOnly(right: 8, top: 4), ), onTap: () => DesktopSettingPage.switch2page(1), onHover: (value) => editHover.value = value, @@ -604,8 +604,17 @@ class _DesktopHomePageState extends State debugPrint("Failed to parse window id '${call.arguments}': $e"); } if (windowId != null) { - await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]); + await rustDeskWinManager.moveTabToNewWindow( + windowId, args[1], args[2]); } + } else if (call.method == kWindowEventOpenMonitorSession) { + final args = jsonDecode(call.arguments); + final windowId = args['window_id'] as int; + final peerId = args['peer_id'] as String; + final display = args['display'] as int; + final displayCount = args['display_count'] as int; + await rustDeskWinManager.openMonitorSession( + windowId, peerId, display, displayCount); } }); _uniLinksSubscription = listenUniLinks(); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a105f2135..905c082a0 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -11,6 +11,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/desktop_render_texture.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; @@ -268,6 +269,7 @@ class _GeneralState extends State<_General> { service(), theme(), hwcodec(), + chooseDisplay(), audio(context), record(context), _Card(title: 'Language', children: [language()]), @@ -376,6 +378,29 @@ class _GeneralState extends State<_General> { ); } + Widget chooseDisplay() { + if (!useTextureRender) return const Offstage(); + + var current = getChooseDisplayBehavior(); + onChanged(String value) { + bind.mainSetOption(key: kKeyChooseDisplayBehavior, value: value); + setState(() {}); + } + + return _Card(title: 'Choose Display Behavior', children: [ + _Radio(context, + value: kChooseDisplayBehaviorSwitch, + groupValue: current, + label: 'Switch Display', + onChanged: onChanged), + _Radio(context, + value: kChooseDisplayBehaviorOpen, + groupValue: current, + label: 'Open in New Window', + onChanged: onChanged), + ]); + } + Widget audio(BuildContext context) { String getDefault() { if (Platform.isWindows) return translate('System Sound'); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index fee5bd8f4..abbb8785d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -28,6 +28,7 @@ import '../widgets/tabbar_widget.dart'; final SimpleWrapper _firstEnterImage = SimpleWrapper(false); +// Used to skip session close if "move to new window" is clicked. final Map closeSessionOnDispose = {}; class RemotePage extends StatefulWidget { @@ -36,6 +37,8 @@ class RemotePage extends StatefulWidget { required this.id, required this.sessionId, required this.tabWindowId, + required this.display, + required this.displays, required this.password, required this.toolbarState, required this.tabController, @@ -46,6 +49,8 @@ class RemotePage extends StatefulWidget { final String id; final SessionID? sessionId; final int? tabWindowId; + final int? display; + final List? displays; final String? password; final ToolbarState toolbarState; final String? switchUuid; @@ -73,7 +78,7 @@ class _RemotePageState extends State late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - late RenderTexture _renderTexture; + final Map _renderTextures = {}; final _blockableOverlayState = BlockableOverlayState(); @@ -109,6 +114,8 @@ class _RemotePageState extends State switchUuid: widget.switchUuid, forceRelay: widget.forceRelay, tabWindowId: widget.tabWindowId, + display: widget.display, + displays: widget.displays, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); @@ -118,9 +125,6 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - // Register texture. - _renderTexture = RenderTexture(); - _renderTexture.create(sessionId); _ffi.ffiModel.updateEventListener(sessionId, widget.id); bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); @@ -207,7 +211,9 @@ class _RemotePageState extends State // https://github.com/flutter/flutter/issues/64935 super.dispose(); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); - await _renderTexture.destroy(closeSession); + for (final texture in _renderTextures.values) { + await texture.destroy(closeSession); + } // ensure we leave this session, this is a double check _ffi.inputModel.enterOrLeave(false); DesktopMultiWindow.removeListener(this); @@ -245,6 +251,7 @@ class _RemotePageState extends State onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Toolbar = func, onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null, + setRemoteState: setState, ); return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, @@ -392,6 +399,38 @@ class _RemotePageState extends State ); } + Map _updateGetRenderTextures(int curDisplay) { + tryCreateTexture(int idx) { + if (!_renderTextures.containsKey(idx)) { + final renderTexture = RenderTexture(); + _renderTextures[idx] = renderTexture; + renderTexture.create(idx, sessionId); + } + } + + tryRemoveTexture(int idx) { + if (_renderTextures.containsKey(idx)) { + _renderTextures[idx]!.destroy(true); + _renderTextures.remove(idx); + } + } + + if (curDisplay == kAllDisplayValue) { + final displays = _ffi.ffiModel.pi.getCurDisplays(); + for (var i = 0; i < displays.length; i++) { + tryCreateTexture(i); + } + } else { + tryCreateTexture(curDisplay); + for (var i = 0; i < _ffi.ffiModel.pi.displays.length; i++) { + if (i != curDisplay) { + tryRemoveTexture(i); + } + } + } + return _renderTextures; + } + Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { @@ -402,16 +441,20 @@ class _RemotePageState extends State Future.delayed(Duration.zero, () { Provider.of(context, listen: false).updateViewStyle(); }); - return ImagePaint( - id: widget.id, - zoomCursor: _zoomCursor, - cursorOverImage: _cursorOverImage, - keyboardEnabled: _keyboardEnabled, - remoteCursorMoved: _remoteCursorMoved, - textureId: _renderTexture.textureId, - useTextureRender: RenderTexture.useTextureRender, - listenerBuilder: (child) => - _buildRawTouchAndPointerRegion(child, enterView, leaveView), + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() => ImagePaint( + id: widget.id, + zoomCursor: _zoomCursor, + cursorOverImage: _cursorOverImage, + keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, + renderTextures: _updateGetRenderTextures(peerDisplay.value), + listenerBuilder: (child) => _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + )), ); })) ]; @@ -447,8 +490,7 @@ class ImagePaint extends StatefulWidget { final RxBool cursorOverImage; final RxBool keyboardEnabled; final RxBool remoteCursorMoved; - final RxInt textureId; - final bool useTextureRender; + final Map renderTextures; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -458,8 +500,7 @@ class ImagePaint extends StatefulWidget { required this.cursorOverImage, required this.keyboardEnabled, required this.remoteCursorMoved, - required this.textureId, - required this.useTextureRender, + required this.renderTextures, this.listenerBuilder}) : super(key: key); @@ -530,27 +571,13 @@ class _ImagePaintState extends State { }); if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { - final imageWidth = c.getDisplayWidth() * s; - final imageHeight = c.getDisplayHeight() * s; - final imageSize = Size(imageWidth, imageHeight); - late final Widget imageWidget; - if (widget.useTextureRender) { - imageWidget = SizedBox( - width: imageWidth, - height: imageHeight, - child: Obx(() => Texture( - textureId: widget.textureId.value, - filterQuality: - isViewOriginal() ? FilterQuality.none : FilterQuality.low, - )), - ); - } else { - imageWidget = CustomPaint( - size: imageSize, - painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), - ); - } - + final paintWidth = c.getDisplayWidth() * s; + final paintHeight = c.getDisplayHeight() * s; + final paintSize = Size(paintWidth, paintHeight); + final paintWidget = useTextureRender + ? _BuildPaintTextureRender( + c, s, Offset.zero, paintSize, isViewOriginal()) + : _buildScrollbarNonTextureRender(m, paintSize, s); return NotificationListener( onNotification: (notification) { final percentX = _horizontal.hasClients @@ -570,43 +597,79 @@ class _ImagePaintState extends State { }, child: mouseRegion( child: Obx(() => _buildCrossScrollbarFromLayout( - context, _buildListener(imageWidget), c.size, imageSize)), + context, _buildListener(paintWidget), c.size, paintSize)), )); } else { - late final Widget imageWidget; if (c.size.width > 0 && c.size.height > 0) { - if (widget.useTextureRender) { - final x = Platform.isLinux ? c.x.toInt().toDouble() : c.x; - final y = Platform.isLinux ? c.y.toInt().toDouble() : c.y; - imageWidget = Stack( - children: [ - Positioned( - left: x, - top: y, - width: c.getDisplayWidth() * s, - height: c.getDisplayHeight() * s, - child: Texture( - textureId: widget.textureId.value, - filterQuality: - isViewOriginal() ? FilterQuality.none : FilterQuality.low, + final paintWidget = useTextureRender + ? _BuildPaintTextureRender( + c, + s, + Offset( + Platform.isLinux ? c.x.toInt().toDouble() : c.x, + Platform.isLinux ? c.y.toInt().toDouble() : c.y, ), - ) - ], - ); - } else { - imageWidget = CustomPaint( - size: Size(c.size.width, c.size.height), - painter: - ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - ); - } - return mouseRegion(child: _buildListener(imageWidget)); + c.size, + isViewOriginal()) + : _buildScrollAuthNonTextureRender(m, c, s); + return mouseRegion(child: _buildListener(paintWidget)); } else { return Container(); } } } + Widget _buildScrollbarNonTextureRender( + ImageModel m, Size imageSize, double s) { + return CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } + + Widget _buildScrollAuthNonTextureRender( + ImageModel m, CanvasModel c, double s) { + return CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } + + Widget _BuildPaintTextureRender( + CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) { + final ffiModel = c.parent.target!.ffiModel; + final displays = ffiModel.pi.getCurDisplays(); + final children = []; + final rect = ffiModel.rect; + if (rect == null) { + return Container(); + } + final curDisplay = ffiModel.pi.currentDisplay; + for (var i = 0; i < displays.length; i++) { + final textureId = widget + .renderTextures[curDisplay == kAllDisplayValue ? i : curDisplay] + ?.textureId; + if (textureId != null) { + children.add(Positioned( + left: (displays[i].x - rect.left) * s + offset.dx, + top: (displays[i].y - rect.top) * s + offset.dy, + width: displays[i].width * s, + height: displays[i].height * s, + child: Obx(() => Texture( + textureId: textureId.value, + filterQuality: + isViewOriginal ? FilterQuality.none : FilterQuality.low, + )), + )); + } + } + return SizedBox( + width: size.width, + height: size.height, + child: Stack(children: children), + ); + } + MouseCursor _buildCursorOfCache( CursorModel cursor, double scale, CursorData? cache) { if (cache == null) { @@ -731,7 +794,11 @@ class _ImagePaintState extends State { ); } - return widget; + return Container( + child: widget, + width: layoutSize.width, + height: layoutSize.height, + ); } Widget _buildListener(Widget child) { @@ -770,9 +837,14 @@ class CursorPaint extends StatelessWidget { double cy = c.y; if (c.viewStyle.style == kRemoteViewStyleOriginal && c.scrollStyle == ScrollStyle.scrollbar) { - final d = c.parent.target!.ffiModel.display; - final imageWidth = d.width * c.scale; - final imageHeight = d.height * c.scale; + final rect = c.parent.target!.ffiModel.rect; + if (rect == null) { + // unreachable! + debugPrint('unreachable! The displays rect is null.'); + return Container(); + } + final imageWidth = rect.width * c.scale; + final imageHeight = rect.height * c.scale; cx = -imageWidth * c.scrollX; cy = -imageHeight * c.scrollY; } diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index feec8de6a..0cc23b39a 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -57,6 +57,8 @@ class _ConnectionTabPageState extends State { peerId = params['id']; final sessionId = params['session_id']; final tabWindowId = params['tab_window_id']; + final display = params['display']; + final displays = params['displays']; if (peerId != null) { ConnectionTypeState.init(peerId!); tabController.onSelected = (id) { @@ -80,6 +82,8 @@ class _ConnectionTabPageState extends State { id: peerId!, sessionId: sessionId == null ? null : SessionID(sessionId), tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), password: params['password'], toolbarState: _toolbarState, tabController: tabController, @@ -109,6 +113,8 @@ class _ConnectionTabPageState extends State { final switchUuid = args['switch_uuid']; final sessionId = args['session_id']; final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; windowOnTop(windowId()); if (tabController.length == 0) { if (Platform.isMacOS && stateGlobal.closeOnFullscreen) { @@ -129,6 +135,8 @@ class _ConnectionTabPageState extends State { id: id, sessionId: sessionId == null ? null : SessionID(sessionId), tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), password: args['password'], toolbarState: _toolbarState, tabController: tabController, @@ -148,6 +156,15 @@ class _ConnectionTabPageState extends State { windowOnTop(windowId()); } return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = tabController.jumpToByKeyAndDisplay(id, display); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; } else if (call.method == kWindowEventGetRemoteList) { return tabController.state.value.tabs .map((e) => e.key) @@ -160,18 +177,20 @@ class _ConnectionTabPageState extends State { .join(';'); } else if (call.method == kWindowEventGetCachedSessionData) { // Ready to show new window and close old tab. - final peerId = call.arguments; + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; try { final remotePage = tabController.state.value.tabs - .firstWhere((tab) => tab.key == peerId) + .firstWhere((tab) => tab.key == id) .page as RemotePage; returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString(); } catch (e) { debugPrint('Failed to get cached session data: $e'); } - if (returnValue != null) { - closeSessionOnDispose[peerId] = false; - tabController.closeBy(peerId); + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); } } _update_remote_count(); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index e7d81dfec..dddbc32a1 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1,10 +1,12 @@ import 'dart:convert'; import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; +import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/consts.dart'; @@ -325,7 +327,8 @@ class RemoteToolbar extends StatefulWidget { final FFI ffi; final ToolbarState state; final Function(Function(bool)) onEnterOrLeaveImageSetter; - final Function() onEnterOrLeaveImageCleaner; + final VoidCallback onEnterOrLeaveImageCleaner; + final Function(VoidCallback) setRemoteState; RemoteToolbar({ Key? key, @@ -334,6 +337,7 @@ class RemoteToolbar extends StatefulWidget { required this.state, required this.onEnterOrLeaveImageSetter, required this.onEnterOrLeaveImageCleaner, + required this.setRemoteState, }) : super(key: key); @override @@ -450,13 +454,17 @@ class _RemoteToolbarState extends State { toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); } - if (PrivacyModeState.find(widget.id).isFalse && pi.displays.length > 1) { - toolbarItems.add( - bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y' - ? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi) - : _MonitorMenu(id: widget.id, ffi: widget.ffi), - ); - } + toolbarItems.add(Obx(() { + if (PrivacyModeState.find(widget.id).isFalse && + pi.displaysCount.value > 1) { + return _MonitorMenu( + id: widget.id, + ffi: widget.ffi, + setRemoteState: widget.setRemoteState); + } else { + return Offstage(); + } + })); toolbarItems .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state)); @@ -581,11 +589,22 @@ class _MobileActionMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget { final String id; final FFI ffi; - const _MonitorMenu({Key? key, required this.id, required this.ffi}) - : super(key: key); + final Function(VoidCallback) setRemoteState; + const _MonitorMenu({ + Key? key, + required this.id, + required this.ffi, + required this.setRemoteState, + }) : super(key: key); + + bool get showMonitorsToolbar => + bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y'; @override - Widget build(BuildContext context) { + Widget build(BuildContext context) => + showMonitorsToolbar ? buildMultiMonitorMenu() : buildMonitorMenu(); + + Widget buildMonitorMenu() { return _IconSubmenuButton( tooltip: 'Select Monitor', icon: icon(), @@ -595,7 +614,80 @@ class _MonitorMenu extends StatelessWidget { menuStyle: MenuStyle( padding: MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), - menuChildren: [Row(children: displays(context))]); + menuChildren: [Row(children: buildMonitorList(false))]); + } + + Widget buildMultiMonitorMenu() { + return Row(children: buildMonitorList(true)); + } + + List buildMonitorList(bool isMulti) { + final List monitorList = []; + final pi = ffi.ffiModel.pi; + + getMonitorText(int i) { + if (i == kAllDisplayValue) { + if (pi.displays.length == 2) { + return '1|2'; + } else { + return 'ALL'; + } + } else { + return (i + 1).toString(); + } + } + + buildMonitorButton(int i) => Obx(() { + RxInt display = CurrentDisplayState.find(id); + return _IconMenuButton( + tooltip: isMulti ? '' : '#${i + 1} monitor', + hMargin: isMulti ? null : 6, + vMargin: isMulti ? null : 12, + topLevel: false, + color: i == display.value + ? _ToolbarTheme.blueColor + : _ToolbarTheme.inactiveColor, + hoverColor: i == display.value + ? _ToolbarTheme.hoverBlueColor + : _ToolbarTheme.hoverInactiveColor, + icon: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _ToolbarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/screen.svg", + colorFilter: + ColorFilter.mode(Colors.white, BlendMode.srcIn), + ), + Obx( + () => Text( + getMonitorText(i), + style: TextStyle( + color: i == display.value + ? _ToolbarTheme.blueColor + : _ToolbarTheme.inactiveColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + onPressed: () => onPressed(i, pi), + ); + }); + + for (int i = 0; i < pi.displays.length; i++) { + monitorList.add(buildMonitorButton(i)); + } + if (pi.isSupportMultiUiSession && pi.displays.length > 1) { + monitorList.add(buildMonitorButton(kAllDisplayValue)); + } + return monitorList; } icon() { @@ -610,7 +702,7 @@ class _MonitorMenu extends StatelessWidget { Obx(() { RxInt display = CurrentDisplayState.find(id); return Text( - '${display.value + 1}/${pi.displays.length}', + '${display.value == kAllDisplayValue ? 'A' : '${display.value + 1}'}/${pi.displays.length}', style: const TextStyle( color: _ToolbarTheme.blueColor, fontSize: 8, @@ -622,48 +714,44 @@ class _MonitorMenu extends StatelessWidget { ); } - List displays(BuildContext context) { - final List rowChildren = []; - final pi = ffi.ffiModel.pi; - for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add(_IconMenuButton( - topLevel: false, - color: _ToolbarTheme.blueColor, - hoverColor: _ToolbarTheme.hoverBlueColor, - tooltip: "#${i + 1} monitor", - hMargin: 6, - vMargin: 12, - icon: Container( - alignment: AlignmentDirectional.center, - constraints: const BoxConstraints(minHeight: _ToolbarTheme.height), - child: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/screen.svg", - colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - Text( - (i + 1).toString(), - style: TextStyle( - color: _ToolbarTheme.blueColor, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - onPressed: () { - _menuDismissCallback(ffi); - RxInt display = CurrentDisplayState.find(id); - if (display.value != i) { - bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i); - } - }, - )); + // Open new tab or window to show this monitor. + // For now just open new window. + openMonitorInNewTabOrWindow(int i, PeerInfo pi) { + if (kWindowId == null) { + // unreachable + debugPrint('openMonitorInNewTabOrWindow, unreachable! kWindowId is null'); + return; + } + DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventOpenMonitorSession, + jsonEncode({ + 'window_id': kWindowId!, + 'peer_id': ffi.id, + 'display': i, + 'display_count': pi.displays.length, + })); + } + + openMonitorInTheSameTab(int i, PeerInfo pi) { + final displays = i == kAllDisplayValue + ? List.generate(pi.displays.length, (index) => index) + : [i]; + bind.sessionSwitchDisplay( + sessionId: ffi.sessionId, value: Int32List.fromList(displays)); + ffi.ffiModel.switchToNewDisplay(i, ffi.sessionId, id); + } + + onPressed(int i, PeerInfo pi) { + _menuDismissCallback(ffi); + RxInt display = CurrentDisplayState.find(id); + if (display.value != i) { + if (pi.isSupportMultiDisplay) { + openMonitorInNewTabOrWindow(i, pi); + } else { + openMonitorInTheSameTab(i, pi); + } } - return rowChildren; } } @@ -1044,14 +1132,14 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { Resolution? _localResolution; late final TextEditingController _customWidth = - TextEditingController(text: display.width.toString()); + TextEditingController(text: rect?.width.toInt().toString() ?? ''); late final TextEditingController _customHeight = - TextEditingController(text: display.height.toString()); + TextEditingController(text: rect?.height.toInt().toString() ?? ''); FFI get ffi => widget.ffi; PeerInfo get pi => widget.ffi.ffiModel.pi; FfiModel get ffiModel => widget.ffi.ffiModel; - Display get display => ffiModel.display; + Rect? get rect => ffiModel.rect; List get resolutions => pi.resolutions; bool get isWayland => bind.mainCurrentIsWayland(); @@ -1063,12 +1151,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { @override Widget build(BuildContext context) { - final isVirtualDisplay = display.isVirtualDisplayResolution; + final isVirtualDisplay = ffiModel.isVirtualDisplayResolution; final visible = ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1); if (!visible) return Offstage(); final showOriginalBtn = - display.isOriginalResolutionSet && !display.isOriginalResolution; + ffiModel.isOriginalResolutionSet && !ffiModel.isOriginalResolution; final showFitLocalBtn = !_isRemoteResolutionFitLocal(); _setGroupValue(); return _SubmenuButton( @@ -1085,12 +1173,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { } _setGroupValue() { + if (pi.currentDisplay == kAllDisplayValue) { + return; + } final lastGroupValue = stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay); if (lastGroupValue == _kCustomResolutionValue) { _groupValue = _kCustomResolutionValue; } else { - _groupValue = '${display.width}x${display.height}'; + _groupValue = '${rect?.width.toInt()}x${rect?.height.toInt()}'; } } @@ -1118,20 +1209,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { _getLocalResolution() { _localResolution = null; - final String currentDisplay = bind.mainGetCurrentDisplay(); - if (currentDisplay.isNotEmpty) { + final String mainDisplay = bind.mainGetMainDisplay(); + if (mainDisplay.isNotEmpty) { try { - final display = json.decode(currentDisplay); + final display = json.decode(mainDisplay); if (display['w'] != null && display['h'] != null) { _localResolution = Resolution(display['w'], display['h']); } } catch (e) { - debugPrint('Failed to decode $currentDisplay, $e'); + debugPrint('Failed to decode $mainDisplay, $e'); } } } _onChanged(BuildContext context, String? value) async { + if (pi.currentDisplay == kAllDisplayValue) { + return; + } stateGlobal.setLastResolutionGroupValue( widget.id, pi.currentDisplay, value); if (value == null) return; @@ -1150,13 +1244,16 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { } if (w != null && h != null) { - if (w != display.width || h != display.height) { + if (w != rect?.width.toInt() || h != rect?.height.toInt()) { await _changeResolution(context, w, h); } } } _changeResolution(BuildContext context, int w, int h) async { + if (pi.currentDisplay == kAllDisplayValue) { + return; + } await bind.sessionChangeResolution( sessionId: ffi.sessionId, display: pi.currentDisplay, @@ -1164,8 +1261,11 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { height: h, ); Future.delayed(Duration(seconds: 3), () async { - final display = ffiModel.display; - if (w == display.width && h == display.height) { + final rect = ffiModel.rect; + if (rect == null) { + return; + } + if (w == rect.width.toInt() && h == rect.height.toInt()) { if (await widget.screenAdjustor.isWindowCanBeAdjusted()) { widget.screenAdjustor.doAdjustWindow(context); } @@ -1175,6 +1275,10 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { Widget _OriginalResolutionMenuButton( BuildContext context, bool showOriginalBtn) { + final display = pi.tryGetDisplayIfNotAllDisplay(); + if (display == null) { + return Offstage(); + } return Offstage( offstage: !showOriginalBtn, child: MenuButton( @@ -1262,7 +1366,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { return null; } - if (display.isVirtualDisplayResolution) { + if (ffiModel.isVirtualDisplayResolution) { return _localResolution!; } @@ -1284,8 +1388,8 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { if (bestFitResolution == null) { return true; } - return bestFitResolution.width == display.width && - bestFitResolution.height == display.height; + return bestFitResolution.width == rect?.width.toInt() && + bestFitResolution.height == rect?.height.toInt(); } } @@ -1361,7 +1465,7 @@ class _KeyboardMenu extends StatelessWidget { continue; } - if (pi.is_wayland && mode.key != _kKeyMapMode) { + if (pi.isWayland && mode.key != _kKeyMapMode) { continue; } @@ -1404,7 +1508,7 @@ class _KeyboardMenu extends StatelessWidget { viewMode() { final ffiModel = ffi.ffiModel; - final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; + final enabled = versionCmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; return CkbMenuButton( value: ffiModel.viewOnly, onChanged: enabled @@ -2037,71 +2141,3 @@ Widget _buildPointerTrackWidget(Widget child, FFI ffi) { ), ); } - -class _MultiMonitorMenu extends StatelessWidget { - final String id; - final FFI ffi; - - const _MultiMonitorMenu({ - Key? key, - required this.id, - required this.ffi, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final List rowChildren = []; - final pi = ffi.ffiModel.pi; - - for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add( - Obx(() { - RxInt display = CurrentDisplayState.find(id); - return _IconMenuButton( - tooltip: "", - topLevel: false, - color: i == display.value - ? _ToolbarTheme.blueColor - : Colors.grey[800]!, - hoverColor: i == display.value - ? _ToolbarTheme.hoverBlueColor - : Colors.grey[850]!, - icon: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _ToolbarTheme.height), - child: Stack( - alignment: Alignment.center, - children: [ - SvgPicture.asset( - "assets/screen.svg", - colorFilter: - ColorFilter.mode(Colors.white, BlendMode.srcIn), - ), - Obx( - () => Text( - (i + 1).toString(), - style: TextStyle( - color: i == display.value - ? _ToolbarTheme.blueColor - : Colors.grey[800]!, - fontSize: 12, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - ), - onPressed: () { - if (display.value != i) { - bind.sessionSwitchDisplay(sessionId: ffi.sessionId, value: i); - } - }, - ); - }), - ); - } - return Row(children: rowChildren); - } -} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index dc58c1b5d..5ce5601a0 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/remote_page.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -176,6 +177,19 @@ class DesktopTabController { jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key), callOnSelected: callOnSelected); + bool jumpToByKeyAndDisplay(String key, int display) { + for (int i = 0; i < state.value.tabs.length; i++) { + final tab = state.value.tabs[i]; + if (tab.key == key) { + final ffi = (tab.page as RemotePage).ffi; + if (ffi.ffiModel.pi.currentDisplay == display) { + return jumpTo(i, callOnSelected: true); + } + } + } + return false; + } + void closeBy(String? key) { if (!isDesktop) return; assert(onRemoved != null); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b38adf63e..249355012 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -755,14 +756,14 @@ void showOptions( if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); } - if (pi.displays.length > 1) { + if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) { final cur = pi.currentDisplay; final children = []; for (var i = 0; i < pi.displays.length; ++i) { children.add(InkWell( onTap: () { if (i == cur) return; - bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: i); + bind.sessionSwitchDisplay(sessionId: gFFI.sessionId, value: Int32List.fromList([i])); gFFI.dialogManager.dismissAll(); }, child: Ink( diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index b18d97b82..4d1c1a8f6 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -4,25 +4,30 @@ import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'; import '../../common.dart'; import './platform_model.dart'; +final useTextureRender = bind.mainUseTextureRender(); + class RenderTexture { final RxInt textureId = RxInt(-1); int _textureKey = -1; + int _display = 0; SessionID? _sessionId; - static final useTextureRender = bind.mainUseTextureRender(); final textureRenderer = TextureRgbaRenderer(); RenderTexture(); - create(SessionID sessionId) { + int get display => _display; + + create(int d, SessionID sessionId) { if (useTextureRender) { + _display = d; _textureKey = bind.getNextTextureKey(); _sessionId = sessionId; textureRenderer.createTexture(_textureKey).then((id) async { if (id != -1) { final ptr = await textureRenderer.getTexturePtr(_textureKey); - platformFFI.registerTexture(sessionId, ptr); + platformFFI.registerTexture(sessionId, display, ptr); textureId.value = id; } }); @@ -32,7 +37,7 @@ class RenderTexture { destroy(bool unregisterTexture) async { if (useTextureRender && _textureKey != -1 && _sessionId != null) { if (unregisterTexture) { - platformFFI.registerTexture(_sessionId!, 0); + platformFFI.registerTexture(_sessionId!, display, 0); } await textureRenderer.closeTexture(_textureKey); _textureKey = -1; diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 971bbb7e5..29c13f625 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -552,22 +552,22 @@ class InputModel { return v; } - Offset setNearestEdge(double x, double y, Display d) { - double left = x - d.x; - double right = d.x + d.width - 1 - x; - double top = y - d.y; - double bottom = d.y + d.height - 1 - y; + Offset setNearestEdge(double x, double y, Rect rect) { + double left = x - rect.left; + double right = rect.right - 1 - x; + double top = y - rect.top; + double bottom = rect.bottom - 1 - y; if (left < right && left < top && left < bottom) { - x = d.x; + x = rect.left; } if (right < left && right < top && right < bottom) { - x = d.x + d.width - 1; + x = rect.right - 1; } if (top < left && top < right && top < bottom) { - y = d.y; + y = rect.top; } if (bottom < left && bottom < right && bottom < top) { - y = d.y + d.height - 1; + y = rect.bottom - 1; } return Offset(x, y); } @@ -711,9 +711,12 @@ class InputModel { final nearThr = 3; var nearRight = (canvasModel.size.width - x) < nearThr; var nearBottom = (canvasModel.size.height - y) < nearThr; - final d = ffiModel.display; - final imageWidth = d.width * canvasModel.scale; - final imageHeight = d.height * canvasModel.scale; + final rect = ffiModel.rect; + if (rect == null) { + return null; + } + final imageWidth = rect.width * canvasModel.scale; + final imageHeight = rect.height * canvasModel.scale; if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { x += imageWidth * canvasModel.scrollX; y += imageHeight * canvasModel.scrollY; @@ -741,11 +744,11 @@ class InputModel { y += step; } } - x += d.x; - y += d.y; + x += rect.left; + y += rect.top; if (onExit) { - final pos = setNearestEdge(x, y, d); + final pos = setNearestEdge(x, y, rect); x = pos.dx; y = pos.dy; } @@ -761,10 +764,10 @@ class InputModel { return null; } - int minX = d.x.toInt(); - int maxX = (d.x + d.width).toInt() - 1; - int minY = d.y.toInt(); - int maxY = (d.y + d.height).toInt() - 1; + int minX = rect.left.toInt(); + int maxX = (rect.left + rect.width).toInt() - 1; + int minY = rect.top.toInt(); + int maxY = (rect.top + rect.height).toInt() - 1; evtX = trySetNearestRange(evtX, minX, maxX, 5); evtY = trySetNearestRange(evtY, minY, maxY, 5); if (kind == kPointerEventKindMouse) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 3c42e02a3..7bb96a99b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -86,7 +87,7 @@ class CachedPeerData { class FfiModel with ChangeNotifier { CachedPeerData cachedPeerData = CachedPeerData(); PeerInfo _pi = PeerInfo(); - Display _display = Display(); + Rect? _rect; var _inputBlocked = false; final _permissions = {}; @@ -103,9 +104,15 @@ class FfiModel with ChangeNotifier { Timer? waitForImageTimer; RxBool waitForFirstImage = true.obs; - Map get permissions => _permissions; + Rect? get rect => _rect; + bool get isOriginalResolutionSet => + _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false; + bool get isVirtualDisplayResolution => + _pi.tryGetDisplayIfNotAllDisplay()?.isVirtualDisplayResolution ?? false; + bool get isOriginalResolution => + _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false; - Display get display => _display; + Map get permissions => _permissions; bool? get secure => _secure; @@ -130,6 +137,24 @@ class FfiModel with ChangeNotifier { sessionId = parent.target!.sessionId; } + Rect? displaysRect() { + final displays = _pi.getCurDisplays(); + if (displays.isEmpty) { + return null; + } + double l = displays[0].x; + double t = displays[0].y; + double r = displays[0].x + displays[0].width; + double b = displays[0].y + displays[0].height; + for (var display in displays.sublist(1)) { + l = min(l, display.x); + t = min(t, display.y); + r = max(r, display.x + display.width); + b = max(b, display.y + display.height); + } + return Rect.fromLTRB(l, t, r, b); + } + toggleTouchMode() { if (!isPeerAndroid) { _touchMode = !_touchMode; @@ -154,7 +179,6 @@ class FfiModel with ChangeNotifier { clear() { _pi = PeerInfo(); - _display = Display(); _secure = null; _direct = null; _inputBlocked = false; @@ -207,8 +231,10 @@ class FfiModel with ChangeNotifier { updateLastCursorId(element); await handleCursorData(element); } - updateLastCursorId(data.lastCursorId); - handleCursorId(data.lastCursorId); + if (data.lastCursorId.isNotEmpty) { + updateLastCursorId(data.lastCursorId); + handleCursorId(data.lastCursorId); + } } // todo: why called by two position @@ -220,11 +246,12 @@ class FfiModel with ChangeNotifier { } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); } else if (name == 'sync_peer_info') { - handleSyncPeerInfo(evt, sessionId); + handleSyncPeerInfo(evt, sessionId, peerId); } else if (name == 'connection_ready') { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { + // switch display is kept for backward compatibility handleSwitchDisplay(evt, sessionId, peerId); } else if (name == 'cursor_data') { updateLastCursorId(evt); @@ -279,7 +306,7 @@ class FfiModel with ChangeNotifier { await bind.sessionSwitchSides(sessionId: sessionId); closeConnection(id: peer_id); } else if (name == 'portable_service_running') { - parent.target?.elevationModel.onPortableServiceRunning(evt); + _handlePortableServiceRunning(peerId, evt); } else if (name == 'on_url_scheme_received') { // currently comes from "_url" ipc of mac and dbus of linux onUrlSchemeReceived(evt); @@ -354,20 +381,65 @@ class FfiModel with ChangeNotifier { platformFFI.setEventCallback(startEventListener(sessionId, peerId)); } - _updateCurDisplay(SessionID sessionId, Display newDisplay) { - if (newDisplay != _display) { - if (newDisplay.x != _display.x || newDisplay.y != _display.y) { - parent.target?.cursorModel - .updateDisplayOrigin(newDisplay.x, newDisplay.y); + _handlePortableServiceRunning(String peerId, Map evt) { + final running = evt['running'] == 'true'; + parent.target?.elevationModel.onPortableServiceRunning(running); + if (running) { + if (pi.primaryDisplay != kInvalidDisplayIndex) { + if (pi.currentDisplay != pi.primaryDisplay) { + // Notify to switch display + msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt', + 'elevated_switch_display_msg', '', parent.target!.dialogManager); + bind.sessionSwitchDisplay( + sessionId: sessionId, + value: Int32List.fromList([pi.primaryDisplay])); + } } - _display = newDisplay; + } + } + + handleAliasChanged(Map evt) { + if (!isDesktop) return; + final String peerId = evt['id']; + final String alias = evt['alias']; + String label = getDesktopTabLabel(peerId, alias); + final rxTabLabel = PeerStringOption.find(evt['id'], 'tabLabel'); + if (rxTabLabel.value != label) { + rxTabLabel.value = label; + } + } + + updateCurDisplay(SessionID sessionId) { + final newRect = displaysRect(); + if (newRect == null) { + return; + } + if (newRect != _rect) { + _rect = newRect; + if (newRect.left != _rect?.left || newRect.top != _rect?.top) { + parent.target?.cursorModel + .updateDisplayOrigin(newRect.left, newRect.top); + } + parent.target?.canvasModel.updateViewStyle(); _updateSessionWidthHeight(sessionId); } } handleSwitchDisplay( Map evt, SessionID sessionId, String peerId) { - _pi.currentDisplay = int.parse(evt['display']); + final curDisplay = int.parse(evt['display']); + + // The message should be handled by the another UI session. + if (_pi.isSupportMultiDisplay) { + if (curDisplay != _pi.currentDisplay) { + return; + } + } + + if (_pi.currentDisplay != kAllDisplayValue) { + _pi.currentDisplay = curDisplay; + } + var newDisplay = Display(); newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x; newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y; @@ -378,11 +450,11 @@ class FfiModel with ChangeNotifier { int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; newDisplay.originalHeight = int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; + _pi.displays[curDisplay] = newDisplay; - _updateCurDisplay(sessionId, newDisplay); - + updateCurDisplay(sessionId); try { - CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + CurrentDisplayState.find(peerId).value = curDisplay; } catch (e) { // } @@ -522,13 +594,30 @@ class FfiModel with ChangeNotifier { } _updateSessionWidthHeight(SessionID sessionId) { - parent.target?.canvasModel.updateViewStyle(); - if (display.width <= 0 || display.height <= 0) { + if (_rect == null) return; + if (_rect!.width <= 0 || _rect!.height <= 0) { debugPrintStack( - label: 'invalid display size (${display.width},${display.height})'); + label: 'invalid display size (${_rect!.width},${_rect!.height})'); } else { - bind.sessionSetSize( - sessionId: sessionId, width: display.width, height: display.height); + final displays = _pi.getCurDisplays(); + if (displays.length == 1) { + bind.sessionSetSize( + sessionId: sessionId, + display: + pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay, + width: _rect!.width.toInt(), + height: _rect!.height.toInt(), + ); + } else { + for (int i = 0; i < displays.length; ++i) { + bind.sessionSetSize( + sessionId: sessionId, + display: i, + width: displays[i].width.toInt(), + height: displays[i].height.toInt(), + ); + } + } } } @@ -541,11 +630,20 @@ class FfiModel with ChangeNotifier { parent.target?.dialogManager.dismissAll(); _pi.version = evt['version']; + _pi.isSupportMultiUiSession = + bind.isSupportMultiUiSession(version: _pi.version); _pi.username = evt['username']; _pi.hostname = evt['hostname']; _pi.platform = evt['platform']; _pi.sasEnabled = evt['sas_enabled'] == 'true'; - _pi.currentDisplay = int.parse(evt['current_display']); + final currentDisplay = int.parse(evt['current_display']); + if (_pi.primaryDisplay == kInvalidDisplayIndex) { + _pi.primaryDisplay = currentDisplay; + } + + if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) { + _pi.currentDisplay = currentDisplay; + } try { CurrentDisplayState.find(peerId).value = _pi.currentDisplay; @@ -569,10 +667,10 @@ class FfiModel with ChangeNotifier { for (int i = 0; i < displays.length; ++i) { _pi.displays.add(evtToDisplay(displays[i])); } - stateGlobal.displaysCount.value = _pi.displays.length; + _pi.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { - _display = _pi.displays[_pi.currentDisplay]; - _updateSessionWidthHeight(sessionId); + // now replaced to _updateCurDisplay + updateCurDisplay(sessionId); } if (displays.isNotEmpty) { _reconnects = 1; @@ -590,12 +688,12 @@ class FfiModel with ChangeNotifier { sessionId: sessionId, arg: 'view-only')); } if (connType == ConnType.defaultConn) { - final platform_additions = evt['platform_additions']; - if (platform_additions != null && platform_additions != '') { + final platformDdditions = evt['platform_additions']; + if (platformDdditions != null && platformDdditions != '') { try { - _pi.platform_additions = json.decode(platform_additions); + _pi.platformDdditions = json.decode(platformDdditions); } catch (e) { - debugPrint('Failed to decode platform_additions $e'); + debugPrint('Failed to decode platformDdditions $e'); } } } @@ -670,7 +768,8 @@ class FfiModel with ChangeNotifier { } /// Handle the peer info synchronization event based on [evt]. - handleSyncPeerInfo(Map evt, SessionID sessionId) async { + handleSyncPeerInfo( + Map evt, SessionID sessionId, String peerId) async { if (evt['displays'] != null) { cachedPeerData.peerInfo['displays'] = evt['displays']; List displays = json.decode(evt['displays']); @@ -679,14 +778,54 @@ class FfiModel with ChangeNotifier { newDisplays.add(evtToDisplay(displays[i])); } _pi.displays = newDisplays; - stateGlobal.displaysCount.value = _pi.displays.length; - if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { - _updateCurDisplay(sessionId, _pi.displays[_pi.currentDisplay]); + _pi.displaysCount.value = _pi.displays.length; + if (_pi.currentDisplay == kAllDisplayValue) { + updateCurDisplay(sessionId); + // to-do: What if the displays are changed? + } else { + if (_pi.currentDisplay >= 0 && + _pi.currentDisplay < _pi.displays.length) { + updateCurDisplay(sessionId); + } else { + if (_pi.displays.isNotEmpty) { + // Notify to switch display + msgBox(sessionId, 'custom-nook-nocancel-hasclose-info', 'Prompt', + 'display_is_plugged_out_msg', '', parent.target!.dialogManager); + final newDisplay = pi.primaryDisplay == kInvalidDisplayIndex + ? 0 + : pi.primaryDisplay; + final displays = newDisplay; + bind.sessionSwitchDisplay( + sessionId: sessionId, value: Int32List.fromList([displays])); + + if (_pi.isSupportMultiUiSession) { + // If the peer supports multi-ui-session, no switch display message will be send back. + // We need to update the display manually. + switchToNewDisplay(newDisplay, sessionId, peerId); + } + } else { + msgBox(sessionId, 'nocancel-error', 'Prompt', 'No Displays', '', + parent.target!.dialogManager); + } + } } } notifyListeners(); } + // Directly switch to the new display without waiting for the response. + switchToNewDisplay(int display, SessionID sessionId, String peerId) { + // no need to wait for the response + pi.currentDisplay = display; + updateCurDisplay(sessionId); + try { + CurrentDisplayState.find(peerId).value = display; + } catch (e) { + // + } + parent.target?.recordingModel.onSwitchDisplay(); + } + updateBlockInputState(Map evt, String peerId) { _inputBlocked = evt['input_state'] == 'on'; notifyListeners(); @@ -709,7 +848,7 @@ class FfiModel with ChangeNotifier { } void setViewOnly(String id, bool value) { - if (version_cmp(_pi.version, '1.2.0') < 0) return; + if (versionCmp(_pi.version, '1.2.0') < 0) return; // tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389 // because below rx not used in mobile version, so not initialized, below code will cause crash // current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!! @@ -749,16 +888,16 @@ class ImageModel with ChangeNotifier { addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); - onRgba(Uint8List rgba) { + onRgba(int display, Uint8List rgba) { final pid = parent.target?.id; img.decodeImageFromPixels( rgba, - parent.target?.ffiModel.display.width ?? 0, - parent.target?.ffiModel.display.height ?? 0, + parent.target?.ffiModel.rect?.width.toInt() ?? 0, + parent.target?.ffiModel.rect?.height.toInt() ?? 0, isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, onPixelsCopied: () { // Unlock the rgba memory from rust codes. - platformFFI.nextRgba(sessionId); + platformFFI.nextRgba(sessionId, display); }).then((image) { if (parent.target?.id != pid) return; try { @@ -1017,20 +1156,20 @@ class CanvasModel with ChangeNotifier { } bool get cursorEmbedded => - parent.target?.ffiModel.display.cursorEmbedded ?? false; + parent.target?.ffiModel._pi.cursorEmbedded ?? false; int getDisplayWidth() { final defaultWidth = (isDesktop || isWebDesktop) ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; - return parent.target?.ffiModel.display.width ?? defaultWidth; + return parent.target?.ffiModel.rect?.width.toInt() ?? defaultWidth; } int getDisplayHeight() { final defaultHeight = (isDesktop || isWebDesktop) ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; - return parent.target?.ffiModel.display.height ?? defaultHeight; + return parent.target?.ffiModel.rect?.height.toInt() ?? defaultHeight; } static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; @@ -1619,7 +1758,27 @@ class QualityMonitorModel with ChangeNotifier { updateQualityStatus(Map evt) { try { if ((evt['speed'] as String).isNotEmpty) _data.speed = evt['speed']; - if ((evt['fps'] as String).isNotEmpty) _data.fps = evt['fps']; + if ((evt['fps'] as String).isNotEmpty) { + final fps = jsonDecode(evt['fps']) as Map; + final pi = parent.target?.ffiModel.pi; + if (pi != null) { + final currentDisplay = pi.currentDisplay; + if (currentDisplay != kAllDisplayValue) { + final fps2 = fps[currentDisplay.toString()]; + if (fps2 != null) { + _data.fps = fps2.toString(); + } + } else if (fps.isNotEmpty) { + final fpsList = []; + for (var i = 0; i < pi.displays.length; i++) { + fpsList.add((fps[i.toString()] ?? 0).toString()); + } + _data.fps = fpsList.join(' '); + } + } else { + _data.fps = null; + } + } if ((evt['delay'] as String).isNotEmpty) _data.delay = evt['delay']; if ((evt['target_bitrate'] as String).isNotEmpty) { _data.targetBitrate = evt['target_bitrate']; @@ -1646,8 +1805,15 @@ class RecordingModel with ChangeNotifier { int? width = parent.target?.canvasModel.getDisplayWidth(); int? height = parent.target?.canvasModel.getDisplayHeight(); if (sessionId == null || width == null || height == null) return; - bind.sessionRecordScreen( - sessionId: sessionId, start: true, width: width, height: height); + final currentDisplay = parent.target?.ffiModel.pi.currentDisplay; + if (currentDisplay != kAllDisplayValue) { + bind.sessionRecordScreen( + sessionId: sessionId, + start: true, + display: currentDisplay!, + width: width, + height: height); + } } toggle() async { @@ -1658,10 +1824,20 @@ class RecordingModel with ChangeNotifier { notifyListeners(); await bind.sessionRecordStatus(sessionId: sessionId, status: _start); if (_start) { - bind.sessionRefresh(sessionId: sessionId); + final pi = parent.target?.ffiModel.pi; + if (pi != null) { + sessionRefreshVideo(sessionId, pi); + } } else { - bind.sessionRecordScreen( - sessionId: sessionId, start: false, width: 0, height: 0); + final currentDisplay = parent.target?.ffiModel.pi.currentDisplay; + if (currentDisplay != kAllDisplayValue) { + bind.sessionRecordScreen( + sessionId: sessionId, + start: false, + display: currentDisplay!, + width: 0, + height: 0); + } } } @@ -1670,8 +1846,15 @@ class RecordingModel with ChangeNotifier { final sessionId = parent.target?.sessionId; if (sessionId == null) return; _start = false; - bind.sessionRecordScreen( - sessionId: sessionId, start: false, width: 0, height: 0); + final currentDisplay = parent.target?.ffiModel.pi.currentDisplay; + if (currentDisplay != kAllDisplayValue) { + bind.sessionRecordScreen( + sessionId: sessionId, + start: false, + display: currentDisplay!, + width: 0, + height: 0); + } } } @@ -1686,9 +1869,7 @@ class ElevationModel with ChangeNotifier { _running = false; } - onPortableServiceRunning(Map evt) { - _running = evt['running'] == 'true'; - } + onPortableServiceRunning(bool running) => _running = running; } enum ConnType { defaultConn, fileTransfer, portForward, rdp } @@ -1751,14 +1932,18 @@ class FFI { } /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. - void start(String id, - {bool isFileTransfer = false, - bool isPortForward = false, - bool isRdp = false, - String? switchUuid, - String? password, - bool? forceRelay, - int? tabWindowId}) { + void start( + String id, { + bool isFileTransfer = false, + bool isPortForward = false, + bool isRdp = false, + String? switchUuid, + String? password, + bool? forceRelay, + int? tabWindowId, + int? display, + List? displays, + }) { closed = false; auditNote = ''; if (isMobile) mobileReset(); @@ -1788,11 +1973,34 @@ class FFI { forceRelay: forceRelay ?? false, password: password ?? '', ); + } else if (display != null) { + if (displays == null) { + debugPrint( + 'Unreachable, failed to add existed session to $id, the displays is null while display is $display'); + return; + } + final addRes = bind.sessionAddExistedSync(id: id, sessionId: sessionId); + if (addRes != '') { + debugPrint( + 'Unreachable, failed to add existed session to $id, $addRes'); + return; + } + bind.sessionTryAddDisplay( + sessionId: sessionId, displays: Int32List.fromList(displays)); + ffiModel.pi.currentDisplay = display; } final stream = bind.sessionStart(sessionId: sessionId, id: id); final cb = ffiModel.startEventListener(sessionId, id); final useTextureRender = bind.mainUseTextureRender(); + // Force refresh displays. + // The controlled side may not refresh the image when the (peer,display) is already subscribed. + if (displays != null) { + for (final display in displays) { + bind.sessionRefresh(sessionId: sessionId, display: display); + } + } + final SimpleWrapper isToNewWindowNotified = SimpleWrapper(false); // Preserved for the rgba data. stream.listen((message) { @@ -1801,8 +2009,9 @@ class FFI { // Session is read to be moved to a new window. // Get the cached data and handle the cached data. Future.delayed(Duration.zero, () async { + final args = jsonEncode({'id': id, 'close': display == null}); final cachedData = await DesktopMultiWindow.invokeMethod( - tabWindowId, kWindowEventGetCachedSessionData, id); + tabWindowId, kWindowEventGetCachedSessionData, args); if (cachedData == null) { // unreachable debugPrint('Unreachable, the cached data is empty.'); @@ -1814,7 +2023,7 @@ class FFI { return; } await ffiModel.handleCachedPeerData(data, id); - await bind.sessionRefresh(sessionId: sessionId); + await sessionRefreshVideo(sessionId, ffiModel.pi); }); isToNewWindowNotified.value = true; } @@ -1836,18 +2045,19 @@ class FFI { await cb(event); } } else if (message is EventToUI_Rgba) { + final display = message.field0; if (useTextureRender) { onEvent2UIRgba(); } else { // Fetch the image buffer from rust codes. - final sz = platformFFI.getRgbaSize(sessionId); + final sz = platformFFI.getRgbaSize(sessionId, display); if (sz == 0) { return; } - final rgba = platformFFI.getRgba(sessionId, sz); + final rgba = platformFFI.getRgba(sessionId, display, sz); if (rgba != null) { onEvent2UIRgba(); - imageModel.onRgba(rgba); + imageModel.onRgba(display, rgba); } } } @@ -1979,22 +2189,73 @@ class Features { bool privacyMode = false; } +const kInvalidDisplayIndex = -1; + class PeerInfo with ChangeNotifier { String version = ''; String username = ''; String hostname = ''; String platform = ''; bool sasEnabled = false; + bool isSupportMultiUiSession = false; int currentDisplay = 0; + int primaryDisplay = kInvalidDisplayIndex; List displays = []; Features features = Features(); List resolutions = []; - Map platform_additions = {}; + Map platformDdditions = {}; + RxInt displaysCount = 0.obs; RxBool isSet = false.obs; - bool get is_wayland => platform_additions['is_wayland'] == true; - bool get is_headless => platform_additions['headless'] == true; + bool get isWayland => platformDdditions['is_wayland'] == true; + bool get isHeadless => platformDdditions['headless'] == true; + + bool get isSupportMultiDisplay => + isDesktop && isSupportMultiUiSession && isChooseDisplayToOpen; + + bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false; + + Display? tryGetDisplay() { + if (displays.isEmpty) { + return null; + } + if (currentDisplay == kAllDisplayValue) { + return displays[0]; + } else { + if (currentDisplay > 0 && currentDisplay < displays.length) { + return displays[currentDisplay]; + } else { + return displays[0]; + } + } + } + + Display? tryGetDisplayIfNotAllDisplay() { + if (displays.isEmpty) { + return null; + } + if (currentDisplay == kAllDisplayValue) { + return null; + } + if (currentDisplay > 0 && currentDisplay < displays.length) { + return displays[currentDisplay]; + } else { + return null; + } + } + + List getCurDisplays() { + if (currentDisplay == kAllDisplayValue) { + return displays; + } else { + if (currentDisplay >= 0 && currentDisplay < displays.length) { + return [displays[currentDisplay]]; + } else { + return []; + } + } + } } const canvasKey = 'canvas'; @@ -2038,8 +2299,8 @@ Future initializeCursorAndCanvas(FFI ffi) async { currentDisplay = p['currentDisplay']; } if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) { - ffi.cursorModel - .updateDisplayOrigin(ffi.ffiModel.display.x, ffi.ffiModel.display.y); + ffi.cursorModel.updateDisplayOrigin( + ffi.ffiModel.rect?.left ?? 0, ffi.ffiModel.rect?.top ?? 0); return; } double xCursor = p['xCursor']; @@ -2047,8 +2308,8 @@ Future initializeCursorAndCanvas(FFI ffi) async { double xCanvas = p['xCanvas']; double yCanvas = p['yCanvas']; double scale = p['scale']; - ffi.cursorModel.updateDisplayOriginWithCursor( - ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor); + ffi.cursorModel.updateDisplayOriginWithCursor(ffi.ffiModel.rect?.left ?? 0, + ffi.ffiModel.rect?.top ?? 0, xCursor, yCursor); ffi.canvasModel.update(xCanvas, yCanvas, scale); } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 4b458f962..cdf7f54b2 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -21,7 +21,8 @@ class RgbaFrame extends Struct { external Pointer data; } -typedef F3 = Pointer Function(Pointer); +typedef F3 = Pointer Function(Pointer, int); +typedef F3Dart = Pointer Function(Pointer, Int32); typedef HandleEvent = Future Function(Map evt); /// FFI wrapper around the native Rust core. @@ -80,12 +81,12 @@ class PlatformFFI { String translate(String name, String locale) => _ffiBind.translate(name: name, locale: locale); - Uint8List? getRgba(SessionID sessionId, int bufSize) { + Uint8List? getRgba(SessionID sessionId, int display, int bufSize) { if (_session_get_rgba == null) return null; final sessionIdStr = sessionId.toString(); var a = sessionIdStr.toNativeUtf8(); try { - final buffer = _session_get_rgba!(a); + final buffer = _session_get_rgba!(a, display); if (buffer == nullptr) { return null; } @@ -96,12 +97,11 @@ class PlatformFFI { } } - int getRgbaSize(SessionID sessionId) => - _ffiBind.sessionGetRgbaSize(sessionId: sessionId); - void nextRgba(SessionID sessionId) => - _ffiBind.sessionNextRgba(sessionId: sessionId); - void registerTexture(SessionID sessionId, int ptr) => - _ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr); + int getRgbaSize(SessionID sessionId, int display) => + _ffiBind.sessionGetRgbaSize(sessionId: sessionId, display: display); + void nextRgba(SessionID sessionId, int display) => _ffiBind.sessionNextRgba(sessionId: sessionId, display: display); + void registerTexture(SessionID sessionId, int display, int ptr) => + _ffiBind.sessionRegisterTexture(sessionId: sessionId, display: display, ptr: ptr); /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { @@ -117,7 +117,7 @@ class PlatformFFI { : DynamicLibrary.process(); debugPrint('initializing FFI $_appType'); try { - _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index e36bef924..2403a794c 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -18,7 +18,6 @@ class StateGlobal { final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteToolBar = false.obs; - final RxInt displaysCount = 0.obs; final svcStatus = SvcStatus.notReady.obs; // Only used for macOS bool closeOnFullscreen = false; diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index a8be78c74..8b80b79bf 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -67,6 +68,44 @@ class RustDeskMultiWindowManager { ); } + // This function must be called in the main window thread. + // Because the _remoteDesktopWindows is managed in that thread. + openMonitorSession( + int windowId, String peerId, int display, int displayCount) async { + if (_remoteDesktopWindows.length > 1) { + for (final windowId in _remoteDesktopWindows) { + if (await DesktopMultiWindow.invokeMethod( + windowId, + kWindowEventActiveDisplaySession, + jsonEncode({ + 'id': peerId, + 'display': display, + }))) { + return; + } + } + } + + final displays = display == kAllDisplayValue + ? List.generate(displayCount, (index) => index) + : [display]; + var params = { + 'type': WindowType.RemoteDesktop.index, + 'id': peerId, + 'tab_window_id': windowId, + 'display': display, + 'displays': displays, + }; + await _newSession( + false, + WindowType.RemoteDesktop, + kWindowEventNewRemoteDesktop, + peerId, + _remoteDesktopWindows, + jsonEncode(params), + ); + } + Future newSessionWindow( WindowType type, String remoteId, String msg, List windows) async { final windowController = await DesktopMultiWindow.createWindow(msg); @@ -148,11 +187,21 @@ class RustDeskMultiWindowManager { bool openInTabs = type != WindowType.RemoteDesktop || mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs); - if (windows.length > 1 || !openInTabs) { - for (final windowId in windows) { - if (await DesktopMultiWindow.invokeMethod( - windowId, kWindowEventActiveSession, remoteId)) { - return MultiWindowCallResult(windowId, null); + if (kOpenSamePeerInNewWindow) { + // Open in new window if the peer is already connected. + // No need to care about the previous session type. + if (type == WindowType.RemoteDesktop && + await bind.sessionGetFlutterOptionByPeerId(id: remoteId, k: '') != + null) { + openInTabs = false; + } + } else { + if (windows.length > 1 || !openInTabs) { + for (final windowId in windows) { + if (await DesktopMultiWindow.invokeMethod( + windowId, kWindowEventActiveSession, remoteId)) { + return MultiWindowCallResult(windowId, null); + } } } } diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index dc6892d61..7cba12d1a 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -5,7 +5,7 @@ use hbb_common::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, }, - ResultType, SessionID, + ResultType, }; use serde_derive::{Deserialize, Serialize}; use std::{ @@ -61,7 +61,7 @@ pub enum ClipboardFile { } struct MsgChannel { - session_uuid: SessionID, + peer_id: String, conn_id: i32, sender: UnboundedSender, receiver: Arc>>, @@ -90,12 +90,12 @@ impl ClipboardFile { } } -pub fn get_client_conn_id(session_uuid: &SessionID) -> Option { +pub fn get_client_conn_id(peer_id: &str) -> Option { VEC_MSG_CHANNEL .read() .unwrap() .iter() - .find(|x| x.session_uuid == session_uuid.to_owned()) + .find(|x| x.peer_id == peer_id) .map(|x| x.conn_id) } @@ -106,13 +106,10 @@ fn get_conn_id() -> i32 { } pub fn get_rx_cliprdr_client( - session_uuid: &SessionID, + peer_id: &str, ) -> (i32, Arc>>) { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); - match lock - .iter() - .find(|x| x.session_uuid == session_uuid.to_owned()) - { + match lock.iter().find(|x| x.peer_id == peer_id) { Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()), None => { let (sender, receiver) = unbounded_channel(); @@ -120,7 +117,7 @@ pub fn get_rx_cliprdr_client( let receiver2 = receiver.clone(); let conn_id = get_conn_id(); let msg_channel = MsgChannel { - session_uuid: session_uuid.to_owned(), + peer_id: peer_id.to_owned(), conn_id, sender, receiver, @@ -140,7 +137,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType { + fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType { let mut frames = Vec::new(); for ref frame in self .encode(ms, frame, STRIDE_ALIGN) @@ -249,7 +249,7 @@ impl EncoderApi for AomEncoder { frames.push(Self::create_frame(frame)); } if frames.len() > 0 { - Ok(Self::create_msg(frames)) + Ok(Self::create_video_frame(frames)) } else { Err(anyhow!("no valid frame")) } @@ -311,16 +311,14 @@ impl AomEncoder { } #[inline] - pub fn create_msg(frames: Vec) -> Message { - let mut msg_out = Message::new(); + pub fn create_video_frame(frames: Vec) -> VideoFrame { let mut vf = VideoFrame::new(); let av1s = EncodedVideoFrames { frames: frames.into(), ..Default::default() }; vf.set_av1s(av1s); - msg_out.set_video_frame(vf); - msg_out + vf } #[inline] diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index cbd433c39..0a3eca248 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -23,8 +23,8 @@ use hbb_common::{ config::PeerConfig, log, message_proto::{ - supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message, - SupportedDecoding, SupportedEncoding, + supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, + SupportedDecoding, SupportedEncoding, VideoFrame, }, sysinfo::{System, SystemExt}, tokio::time::Instant, @@ -60,7 +60,7 @@ pub trait EncoderApi { where Self: Sized; - fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType; + fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType; fn use_yuv(&self) -> bool; diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index c1fbdfa6e..333f85b98 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -8,7 +8,7 @@ use hbb_common::{ bytes::Bytes, config::HwCodecConfig, log, - message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}, + message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}, ResultType, }; use hwcodec::{ @@ -92,12 +92,7 @@ impl EncoderApi for HwEncoder { } } - fn encode_to_message( - &mut self, - frame: &[u8], - _ms: i64, - ) -> ResultType { - let mut msg_out = Message::new(); + fn encode_to_message(&mut self, frame: &[u8], _ms: i64) -> ResultType { let mut vf = VideoFrame::new(); let mut frames = Vec::new(); for frame in self.encode(frame).with_context(|| "Failed to encode")? { @@ -117,8 +112,7 @@ impl EncoderApi for HwEncoder { DataFormat::H264 => vf.set_h264s(frames), DataFormat::H265 => vf.set_h265s(frames), } - msg_out.set_video_frame(vf); - Ok(msg_out) + Ok(vf) } else { Err(anyhow!("no valid frame")) } diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 1bdde0ca6..edf620465 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -4,7 +4,7 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::log; -use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; +use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use hbb_common::ResultType; use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality}; @@ -172,7 +172,7 @@ impl EncoderApi for VpxEncoder { } } - fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType { + fn encode_to_message(&mut self, frame: &[u8], ms: i64) -> ResultType { let mut frames = Vec::new(); for ref frame in self .encode(ms, frame, STRIDE_ALIGN) @@ -186,7 +186,7 @@ impl EncoderApi for VpxEncoder { // to-do: flush periodically, e.g. 1 second if frames.len() > 0 { - Ok(VpxEncoder::create_msg(self.id, frames)) + Ok(VpxEncoder::create_video_frame(self.id, frames)) } else { Err(anyhow!("no valid frame")) } @@ -266,8 +266,10 @@ impl VpxEncoder { } #[inline] - pub fn create_msg(codec_id: VpxVideoCodecId, frames: Vec) -> Message { - let mut msg_out = Message::new(); + pub fn create_video_frame( + codec_id: VpxVideoCodecId, + frames: Vec, + ) -> VideoFrame { let mut vf = VideoFrame::new(); let vpxs = EncodedVideoFrames { frames: frames.into(), @@ -277,8 +279,7 @@ impl VpxEncoder { VpxVideoCodecId::VP8 => vf.set_vp8s(vpxs), VpxVideoCodecId::VP9 => vf.set_vp9s(vpxs), } - msg_out.set_video_frame(vf); - msg_out + vf } #[inline] diff --git a/src/client.rs b/src/client.rs index 4ecb0a0fb..f7c6c6d96 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,10 +3,7 @@ use std::{ net::SocketAddr, ops::Deref, str::FromStr, - sync::{ - atomic::{AtomicUsize, Ordering}, - mpsc, Arc, Mutex, RwLock, - }, + sync::{mpsc, Arc, Mutex, RwLock}, }; pub use async_trait::async_trait; @@ -60,6 +57,7 @@ use scrap::{ use crate::{ common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, is_keyboard_mode_supported, + ui_session_interface::{InvokeUiSession, Session}, }; #[cfg(not(feature = "flutter"))] @@ -675,9 +673,12 @@ impl Client { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn try_stop_clipboard(_self_uuid: &uuid::Uuid) { + fn try_stop_clipboard(_self_id: &str) { #[cfg(feature = "flutter")] - if crate::flutter::sessions::other_sessions_running(_self_uuid) { + if crate::flutter::sessions::other_sessions_running( + _self_id.to_string(), + ConnType::DEFAULT_CONN, + ) { return; } TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; @@ -1523,6 +1524,15 @@ impl LoginConfigHandler { msg_out } + /// Create a [`Message`] for refreshing video. + pub fn refresh_display(display: usize) -> Message { + let mut misc = Misc::new(); + misc.set_refresh_video_display(display as _); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + msg_out + } + /// Create a [`Message`] for saving custom image quality. /// /// # Arguments @@ -1789,89 +1799,158 @@ impl LoginConfigHandler { /// Media data. pub enum MediaData { - VideoQueue, + VideoQueue(usize), VideoFrame(Box), AudioFrame(Box), AudioFormat(AudioFormat), - Reset, - RecordScreen(bool, i32, i32, String), + Reset(usize), + RecordScreen(bool, usize, i32, i32, String), } pub type MediaSender = mpsc::Sender; +struct VideoHandlerController { + handler: VideoHandler, + count: u128, + duration: std::time::Duration, + skip_beginning: u32, +} + /// Start video and audio thread. /// Return two [`MediaSender`], they should be given to the media producer. /// /// # Arguments /// /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. -pub fn start_video_audio_threads( +pub fn start_video_audio_threads( + session: Session, video_callback: F, ) -> ( MediaSender, MediaSender, - Arc>, - Arc, + Arc>>>, + Arc>>, ) where - F: 'static + FnMut(&mut scrap::ImageRgb) + Send, + F: 'static + FnMut(usize, &mut scrap::ImageRgb) + Send, + T: InvokeUiSession, { let (video_sender, video_receiver) = mpsc::channel::(); - let video_queue = Arc::new(ArrayQueue::::new(VIDEO_QUEUE_SIZE)); - let video_queue_cloned = video_queue.clone(); + let video_queue_map: Arc>>> = Default::default(); + let video_queue_map_cloned = video_queue_map.clone(); let mut video_callback = video_callback; - let mut duration = std::time::Duration::ZERO; - let mut count = 0; - let fps = Arc::new(AtomicUsize::new(0)); - let decode_fps = fps.clone(); - let mut skip_beginning = 0; + let fps_map = Arc::new(RwLock::new(HashMap::new())); + let decode_fps_map = fps_map.clone(); std::thread::spawn(move || { #[cfg(windows)] sync_cpu_usage(); - let mut video_handler = VideoHandler::new(); + let mut handler_controller_map = Vec::new(); + // let mut count = Vec::new(); + // let mut duration = std::time::Duration::ZERO; + // let mut skip_beginning = Vec::new(); loop { if let Ok(data) = video_receiver.recv() { match data { - MediaData::VideoFrame(_) | MediaData::VideoQueue => { - let vf = if let MediaData::VideoFrame(vf) = data { - *vf - } else { - if let Some(vf) = video_queue.pop() { - vf - } else { + MediaData::VideoFrame(_) | MediaData::VideoQueue(_) => { + let vf = match data { + MediaData::VideoFrame(vf) => *vf, + MediaData::VideoQueue(display) => { + if let Some(video_queue) = + video_queue_map.read().unwrap().get(&display) + { + if let Some(vf) = video_queue.pop() { + vf + } else { + continue; + } + } else { + continue; + } + } + _ => { + // unreachable!(); continue; } }; + let display = vf.display as usize; let start = std::time::Instant::now(); - if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(&mut video_handler.rgb); - // fps calculation - // The first frame will be very slow - if skip_beginning < 5 { - skip_beginning += 1; - continue; + if handler_controller_map.len() <= display { + for _i in handler_controller_map.len()..=display { + handler_controller_map.push(VideoHandlerController { + handler: VideoHandler::new(), + count: 0, + duration: std::time::Duration::ZERO, + skip_beginning: 0, + }); } - duration += start.elapsed(); - count += 1; - if count % 10 == 0 { - fps.store( - (count * 1000 / duration.as_millis()) as usize, - Ordering::Relaxed, - ); - } - // Clear to get real-time fps - if count > 150 { - count = 0; - duration = Duration::ZERO; + } + if let Some(handler_controller) = handler_controller_map.get_mut(display) { + match handler_controller.handler.handle_frame(vf) { + Ok(true) => { + video_callback(display, &mut handler_controller.handler.rgb); + + // fps calculation + // The first frame will be very slow + if handler_controller.skip_beginning < 5 { + handler_controller.skip_beginning += 1; + continue; + } + + handler_controller.duration += start.elapsed(); + handler_controller.count += 1; + if handler_controller.count % 10 == 0 { + fps_map.write().unwrap().insert( + display, + (handler_controller.count * 1000 + / handler_controller.duration.as_millis()) + as usize, + ); + } + // Clear to get real-time fps + if handler_controller.count > 150 { + handler_controller.count = 0; + handler_controller.duration = Duration::ZERO; + } + } + Err(e) => { + // This is a simple workaround. + // + // I only see the following error: + // FailedCall("errcode=1 scrap::common::vpxcodec:libs\\scrap\\src\\common\\vpxcodec.rs:433:9") + // When switching from all displays to one display, the error occurs. + // eg: + // 1. Connect to a device with two displays (A and B). + // 2. Switch to display A. The error occurs. + // 3. If the error does not occur. Switch from A to display B. The error occurs. + // + // to-do: fix the error + log::error!("handle video frame error, {}", e); + session.refresh_video(display); + } + _ => {} } } } - MediaData::Reset => { - video_handler.reset(); + MediaData::Reset(display) => { + if let Some(handler_controler) = handler_controller_map.get_mut(display) { + handler_controler.handler.reset(); + } } - MediaData::RecordScreen(start, w, h, id) => { - video_handler.record_screen(start, w, h, id) + MediaData::RecordScreen(start, display, w, h, id) => { + if handler_controller_map.len() == 1 { + // Compatible with the sciter version(single ui session). + // For the sciter version, there're no multi-ui-sessions for one connection. + // The display is always 0, video_handler_controllers.len() is always 1. So we use the first video handler. + handler_controller_map[0] + .handler + .record_screen(start, w, h, id); + } else { + if let Some(handler_controler) = handler_controller_map.get_mut(display) + { + handler_controler.handler.record_screen(start, w, h, id); + } + } } _ => {} } @@ -1882,7 +1961,12 @@ where log::info!("Video decoder loop exits"); }); let audio_sender = start_audio_thread(); - return (video_sender, audio_sender, video_queue_cloned, decode_fps); + return ( + video_sender, + audio_sender, + video_queue_map_cloned, + decode_fps_map, + ); } /// Start an audio thread @@ -2500,7 +2584,7 @@ pub enum Data { SetConfirmOverrideFile((i32, i32, bool, bool, bool)), AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), - RecordScreen(bool, i32, i32, String), + RecordScreen(bool, usize, i32, i32, String), ElevateDirect, ElevateWithLogon(String, String), NewVoiceCall, diff --git a/src/client/helper.rs b/src/client/helper.rs index 61844d908..09a8d5d24 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use hbb_common::{ get_time, message_proto::{Message, VoiceCallRequest, VoiceCallResponse}, @@ -7,7 +8,7 @@ use scrap::CodecFormat; #[derive(Debug, Default)] pub struct QualityStatus { pub speed: Option, - pub fps: Option, + pub fps: HashMap, pub delay: Option, pub target_bitrate: Option, pub codec_format: Option, diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 28edf0e52..151bee0cc 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,33 +1,41 @@ -use std::collections::HashMap; -use std::num::NonZeroI64; -use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, +use std::{ + collections::HashMap, + num::NonZeroI64, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, + }, }; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, ContextSend}; use crossbeam_queue::ArrayQueue; -use hbb_common::config::{PeerConfig, TransferSerde}; -use hbb_common::fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, -}; -use hbb_common::message_proto::permission_info::Permission; -use hbb_common::protobuf::Message as _; -use hbb_common::rendezvous_proto::ConnType; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::sleep; #[cfg(not(target_os = "ios"))] use hbb_common::tokio::sync::mpsc::error::TryRecvError; #[cfg(windows)] use hbb_common::tokio::sync::Mutex as TokioMutex; -use hbb_common::tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, +use hbb_common::{ + allow_err, + config::{PeerConfig, TransferSerde}, + fs, + fs::{ + can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, + RemoveJobMeta, + }, + get_time, log, + message_proto::permission_info::Permission, + message_proto::*, + protobuf::Message as _, + rendezvous_proto::ConnType, + tokio::{ + self, + sync::mpsc, + time::{self, Duration, Instant, Interval}, + }, + Stream, }; -use hbb_common::{allow_err, fs, get_time, log, message_proto::*, Stream}; use scrap::CodecFormat; use crate::client::{ @@ -43,7 +51,7 @@ use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, - video_queue: Arc>, + video_queue_map: Arc>>>, video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, @@ -61,27 +69,27 @@ pub struct Remote { #[cfg(windows)] client_conn_id: i32, // used for file clipboard data_count: Arc, - frame_count: Arc, + frame_count_map: Arc>>, video_format: CodecFormat, elevation_requested: bool, - fps_control: FpsControl, - decode_fps: Arc, + fps_control_map: HashMap, + decode_fps_map: Arc>>, } impl Remote { pub fn new( handler: Session, - video_queue: Arc>, + video_queue: Arc>>>, video_sender: MediaSender, audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, - frame_count: Arc, - decode_fps: Arc, + frame_count_map: Arc>>, + decode_fps: Arc>>, ) -> Self { Self { handler, - video_queue, + video_queue_map: video_queue, video_sender, audio_sender, receiver, @@ -96,13 +104,13 @@ impl Remote { #[cfg(windows)] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), - frame_count, + frame_count_map, video_format: CodecFormat::Unknown, stop_voice_call_sender: None, voice_call_request_timestamp: None, elevation_requested: false, - fps_control: Default::default(), - decode_fps, + fps_control_map: Default::default(), + decode_fps_map: decode_fps, } } @@ -152,7 +160,7 @@ impl Remote { || self.handler.is_rdp(); if !is_conn_not_default { (self.client_conn_id, rx_clip_client_lock) = - clipboard::get_rx_cliprdr_client(&self.handler.session_id); + clipboard::get_rx_cliprdr_client(&self.handler.id); }; } #[cfg(windows)] @@ -229,12 +237,18 @@ impl Remote { let mut speed = self.data_count.swap(0, Ordering::Relaxed); speed = speed * 1000 / elapsed as usize; let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let mut fps = self.frame_count.swap(0, Ordering::Relaxed) as _; - // Correcting the inaccuracy of status_timer - fps = fps * 1000 / elapsed as i32; + + let mut frame_count_map_write = self.frame_count_map.write().unwrap(); + let frame_count_map = frame_count_map_write.clone(); + frame_count_map_write.values_mut().for_each(|v| *v = 0); + drop(frame_count_map_write); + let fps = frame_count_map.iter().map(|(k, v)| { + // Correcting the inaccuracy of status_timer + (k.clone(), (*v as i32) * 1000 / elapsed as i32) + }).collect::>(); self.handler.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), + speed: Some(speed), + fps, ..Default::default() }); } @@ -260,7 +274,7 @@ impl Remote { #[cfg(not(any(target_os = "android", target_os = "ios")))] if _set_disconnected_ok { - Client::try_stop_clipboard(&self.handler.session_id); + Client::try_stop_clipboard(&self.handler.id); } #[cfg(windows)] @@ -760,10 +774,10 @@ impl Remote { } } } - Data::RecordScreen(start, w, h, id) => { + Data::RecordScreen(start, display, w, h, id) => { let _ = self .video_sender - .send(MediaData::RecordScreen(start, w, h, id)); + .send(MediaData::RecordScreen(start, display, w, h, id)); } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -904,89 +918,100 @@ impl Remote { None => false, } } + #[inline] fn fps_control(&mut self, direct: bool) { - let len = self.video_queue.len(); - let ctl = &mut self.fps_control; - // Current full speed decoding fps - let decode_fps = self.decode_fps.load(std::sync::atomic::Ordering::Relaxed); - if decode_fps == 0 { - return; - } - let limited_fps = if direct { - decode_fps * 9 / 10 // 30 got 27 - } else { - decode_fps * 4 / 5 // 30 got 24 - }; - // send full speed fps - let version = self.handler.lc.read().unwrap().version; - let max_encode_speed = 144 * 10 / 9; - if version >= hbb_common::get_version_number("1.2.1") - && (ctl.last_full_speed_fps.is_none() // First time - || ((ctl.last_full_speed_fps.unwrap_or_default() - decode_fps as i32).abs() >= 5 // diff 5 - && !(decode_fps > max_encode_speed // already exceed max encoding speed - && ctl.last_full_speed_fps.unwrap_or_default() > max_encode_speed as i32))) - { - let mut misc = Misc::new(); - misc.set_full_speed_fps(decode_fps as _); - let mut msg = Message::new(); - msg.set_misc(misc); - self.sender.send(Data::Message(msg)).ok(); - ctl.last_full_speed_fps = Some(decode_fps as _); - } - // decrease judgement - let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 }; // 500ms - let should_decrease = len >= debounce // exceed debounce - && len > ctl.last_queue_size + 5 // still caching - && !ctl.last_custom_fps.unwrap_or(i32::MAX) < limited_fps as i32; // NOT already set a smaller one + let decode_fps_read = self.decode_fps_map.read().unwrap(); + for (display, decode_fps) in decode_fps_read.iter() { + let video_queue_map_read = self.video_queue_map.read().unwrap(); + let Some(video_queue) = video_queue_map_read.get(display) else { + continue; + }; - // increase judgement - if len <= 1 { - ctl.idle_counter += 1; - } else { - ctl.idle_counter = 0; - } - let mut should_increase = false; - if let Some(last_custom_fps) = ctl.last_custom_fps { - // ever set - if last_custom_fps + 5 < limited_fps as i32 && ctl.idle_counter > 3 { - // limited_fps is 5 larger than last set, and idle time is more than 3 seconds - should_increase = true; + if !self.fps_control_map.contains_key(display) { + self.fps_control_map.insert(*display, FpsControl::default()); } - } - if should_decrease || should_increase { - // limited_fps to ensure decoding is faster than encoding - let mut custom_fps = limited_fps as i32; - if custom_fps < 1 { - custom_fps = 1; - } - // send custom fps - let mut misc = Misc::new(); - if version > hbb_common::get_version_number("1.2.1") { - // avoid confusion with custom image quality fps - misc.set_auto_adjust_fps(custom_fps as _); + let Some(ctl) = self.fps_control_map.get_mut(display) else { + return; + }; + + let len = video_queue.len(); + let decode_fps = *decode_fps; + let limited_fps = if direct { + decode_fps * 9 / 10 // 30 got 27 } else { - misc.set_option(OptionMessage { - custom_fps, - ..Default::default() - }); + decode_fps * 4 / 5 // 30 got 24 + }; + // send full speed fps + let version = self.handler.lc.read().unwrap().version; + let max_encode_speed = 144 * 10 / 9; + if version >= hbb_common::get_version_number("1.2.1") + && (ctl.last_full_speed_fps.is_none() // First time + || ((ctl.last_full_speed_fps.unwrap_or_default() - decode_fps as i32).abs() >= 5 // diff 5 + && !(decode_fps > max_encode_speed // already exceed max encoding speed + && ctl.last_full_speed_fps.unwrap_or_default() > max_encode_speed as i32))) + { + let mut misc = Misc::new(); + misc.set_full_speed_fps(decode_fps as _); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + ctl.last_full_speed_fps = Some(decode_fps as _); + } + // decrease judgement + let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 }; // 500ms + let should_decrease = len >= debounce // exceed debounce + && len > ctl.last_queue_size + 5 // still caching + && !ctl.last_custom_fps.unwrap_or(i32::MAX) < limited_fps as i32; // NOT already set a smaller one + + // increase judgement + if len <= 1 { + ctl.idle_counter += 1; + } else { + ctl.idle_counter = 0; + } + let mut should_increase = false; + if let Some(last_custom_fps) = ctl.last_custom_fps { + // ever set + if last_custom_fps + 5 < limited_fps as i32 && ctl.idle_counter > 3 { + // limited_fps is 5 larger than last set, and idle time is more than 3 seconds + should_increase = true; + } + } + if should_decrease || should_increase { + // limited_fps to ensure decoding is faster than encoding + let mut custom_fps = limited_fps as i32; + if custom_fps < 1 { + custom_fps = 1; + } + // send custom fps + let mut misc = Misc::new(); + if version > hbb_common::get_version_number("1.2.1") { + // avoid confusion with custom image quality fps + misc.set_auto_adjust_fps(custom_fps as _); + } else { + misc.set_option(OptionMessage { + custom_fps, + ..Default::default() + }); + } + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); + ctl.last_queue_size = len; + ctl.last_custom_fps = Some(custom_fps); + } + // send refresh + if ctl.refresh_times < 10 // enough + && (len > video_queue.capacity() / 2 + && (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 30)) + { + // Refresh causes client set_display, left frames cause flickering. + while let Some(_) = video_queue.pop() {} + self.handler.refresh_video(*display); + ctl.refresh_times += 1; + ctl.last_refresh_instant = Instant::now(); } - let mut msg = Message::new(); - msg.set_misc(misc); - self.sender.send(Data::Message(msg)).ok(); - ctl.last_queue_size = len; - ctl.last_custom_fps = Some(custom_fps); - } - // send refresh - if ctl.refresh_times < 10 // enough - && (len > self.video_queue.capacity() / 2 - && (ctl.refresh_times == 0 || ctl.last_refresh_instant.elapsed().as_secs() > 30)) - { - // Refresh causes client set_display, left frames cause flickering. - while let Some(_) = self.video_queue.pop() {} - self.handler.refresh_video(); - ctl.refresh_times += 1; - ctl.last_refresh_instant = Instant::now(); } } @@ -1008,14 +1033,27 @@ impl Remote { ..Default::default() }) }; + + let display = vf.display as usize; + let mut video_queue_write = self.video_queue_map.write().unwrap(); + if !video_queue_write.contains_key(&display) { + video_queue_write.insert( + display, + ArrayQueue::::new(crate::client::VIDEO_QUEUE_SIZE), + ); + } if Self::contains_key_frame(&vf) { - while let Some(_) = self.video_queue.pop() {} + if let Some(video_queue) = video_queue_write.get_mut(&display) { + while let Some(_) = video_queue.pop() {} + } self.video_sender .send(MediaData::VideoFrame(Box::new(vf))) .ok(); } else { - self.video_queue.force_push(vf); - self.video_sender.send(MediaData::VideoQueue).ok(); + if let Some(video_queue) = video_queue_write.get_mut(&display) { + video_queue.force_push(vf); + } + self.video_sender.send(MediaData::VideoQueue(display)).ok(); } } Some(message::Union::Hash(hash)) => { @@ -1297,7 +1335,9 @@ impl Remote { } Some(misc::Union::SwitchDisplay(s)) => { self.handler.handle_peer_switch_display(&s); - self.video_sender.send(MediaData::Reset).ok(); + self.video_sender + .send(MediaData::Reset(s.display as _)) + .ok(); if s.width > 0 && s.height > 0 { self.handler.set_display( s.x, @@ -1674,7 +1714,7 @@ impl Remote { #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id - != clipboard::get_client_conn_id(&crate::flutter::get_cur_session_id()).unwrap_or(0) + != clipboard::get_client_conn_id(&crate::flutter::get_cur_peer_id()).unwrap_or(0) { return; } diff --git a/src/common.rs b/src/common.rs index e5599389c..3b2f5ca3e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -54,6 +54,8 @@ pub const PLATFORM_LINUX: &str = "Linux"; pub const PLATFORM_MACOS: &str = "Mac OS"; pub const PLATFORM_ANDROID: &str = "Android"; +const MIN_VER_MULTI_UI_SESSION: &str = "1.2.4"; + pub mod input { pub const MOUSE_TYPE_MOVE: i32 = 0; pub const MOUSE_TYPE_DOWN: i32 = 1; @@ -120,6 +122,16 @@ pub fn set_server_running(b: bool) { *SERVER_RUNNING.write().unwrap() = b; } +#[inline] +pub fn is_support_multi_ui_session(ver: &str) -> bool { + is_support_multi_ui_session_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_multi_ui_session_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION) +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { @@ -780,13 +792,17 @@ pub fn get_sysinfo() -> serde_json::Value { } let hostname = hostname(); // sys.hostname() return localhost on android in my test use serde_json::json; - let mut out = json!({ + #[cfg(any(target_os = "android", target_os = "ios"))] + let out; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let mut out; + out = json!({ "cpu": format!("{cpu}{num_cpus}/{num_pcpus} cores"), "memory": format!("{memory}GB"), "os": os, "hostname": hostname, }); - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { out["username"] = json!(crate::platform::get_active_username()); } diff --git a/src/flutter.rs b/src/flutter.rs index c9863078a..237775003 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -18,8 +18,6 @@ use hbb_common::{ }; use serde_json::json; -#[cfg(not(feature = "flutter_texture_render"))] -use std::sync::atomic::{AtomicBool, Ordering}; use std::{ collections::HashMap, ffi::CString, @@ -150,25 +148,40 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { // Afterwards the vector will be dropped and thus freed. } +#[derive(Default)] +struct SessionHandler { + event_stream: Option>, + #[cfg(feature = "flutter_texture_render")] + notify_rendered: bool, + #[cfg(feature = "flutter_texture_render")] + renderer: VideoRenderer, +} + #[cfg(feature = "flutter_texture_render")] #[derive(Default, Clone)] pub struct FlutterHandler { - pub event_stream: Arc>>>, - notify_rendered: Arc>, - renderer: Arc>, + // ui session id -> display handler data + session_handlers: Arc>>, peer_info: Arc>, + #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] hooks: Arc>>, } #[cfg(not(feature = "flutter_texture_render"))] #[derive(Default, Clone)] -pub struct FlutterHandler { - pub event_stream: Arc>>>, +struct RgbaData { // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. // We must check the `rgba_valid` before reading [rgba]. - pub rgba: Arc>>, - pub rgba_valid: Arc, + data: Vec, + valid: bool, +} + +#[cfg(not(feature = "flutter_texture_render"))] +#[derive(Default, Clone)] +pub struct FlutterHandler { + session_handlers: Arc>>, + display_rgbas: Arc>>, peer_info: Arc>, #[cfg(not(any(target_os = "android", target_os = "ios")))] hooks: Arc>>, @@ -184,14 +197,22 @@ pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn( dst_rgba_stride: c_int, ); +#[cfg(feature = "flutter_texture_render")] +pub(super) type TextureRgbaPtr = usize; + +#[cfg(feature = "flutter_texture_render")] +struct DisplaySessionInfo { + // TextureRgba pointer in flutter native. + texture_rgba_ptr: TextureRgbaPtr, + size: (usize, usize), +} + // Video Texture Renderer in Flutter #[cfg(feature = "flutter_texture_render")] #[derive(Clone)] struct VideoRenderer { - // TextureRgba pointer in flutter native. - ptr: Arc>, - width: usize, - height: usize, + is_support_multi_ui_session: bool, + map_display_sessions: Arc>>, on_rgba_func: Option>, } @@ -217,9 +238,8 @@ impl Default for VideoRenderer { } }; Self { - ptr: Default::default(), - width: 0, - height: 0, + map_display_sessions: Default::default(), + is_support_multi_ui_session: false, on_rgba_func, } } @@ -228,33 +248,74 @@ impl Default for VideoRenderer { #[cfg(feature = "flutter_texture_render")] impl VideoRenderer { #[inline] - pub fn set_size(&mut self, width: usize, height: usize) { - self.width = width; - self.height = height; + fn set_size(&mut self, display: usize, width: usize, height: usize) { + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if let Some(info) = sessions_lock.get_mut(&display) { + info.size = (width, height); + } else { + sessions_lock.insert( + display, + DisplaySessionInfo { + texture_rgba_ptr: usize::default(), + size: (width, height), + }, + ); + } } - pub fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { - let ptr = self.ptr.read().unwrap(); - if *ptr == usize::default() { + fn register_texture(&self, display: usize, ptr: usize) { + let mut sessions_lock = self.map_display_sessions.write().unwrap(); + if ptr == 0 { + sessions_lock.remove(&display); + } else { + if let Some(info) = sessions_lock.get_mut(&display) { + if info.texture_rgba_ptr != 0 && info.texture_rgba_ptr != ptr as TextureRgbaPtr { + log::error!("unreachable, texture_rgba_ptr is not null and not equal to ptr"); + } + info.texture_rgba_ptr = ptr as _; + } else { + if ptr != 0 { + sessions_lock.insert( + display, + DisplaySessionInfo { + texture_rgba_ptr: ptr as _, + size: (0, 0), + }, + ); + } + } + } + } + + pub fn on_rgba(&self, display: usize, rgba: &scrap::ImageRgb) { + let read_lock = self.map_display_sessions.read().unwrap(); + let opt_info = if !self.is_support_multi_ui_session { + read_lock.values().next() + } else { + read_lock.get(&display) + }; + let Some(info) = opt_info else { + return; + }; + if info.texture_rgba_ptr == usize::default() { return; } // It is also Ok to skip this check. - if self.width != rgba.w || self.height != rgba.h { + if info.size.0 != rgba.w || info.size.1 != rgba.h { log::error!( "width/height mismatch: ({},{}) != ({},{})", - self.width, - self.height, + info.size.0, + info.size.1, rgba.w, rgba.h ); return; } - if let Some(func) = &self.on_rgba_func { unsafe { func( - *ptr as _, + info.texture_rgba_ptr as _, rgba.raw.as_ptr() as _, rgba.raw.len() as _, rgba.w as _, @@ -266,34 +327,42 @@ impl VideoRenderer { } } +impl SessionHandler { + pub fn on_waiting_for_image_dialog_show(&mut self) { + #[cfg(any(feature = "flutter_texture_render"))] + { + self.notify_rendered = false; + } + // rgba array render will notify every frame + } +} + impl FlutterHandler { - /// Push an event to the event queue. - /// An event is stored as json in the event queue. + /// Push an event to all the event queues. + /// An event is stored as json in the event queues. /// /// # Arguments /// /// * `name` - The name of the event. /// * `event` - Fields of the event content. - pub fn push_event(&self, name: &str, event: Vec<(&str, &str)>) -> Option { + pub fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); debug_assert!(h.get("name").is_none()); h.insert("name", name); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); - Some( - self.event_stream - .read() - .unwrap() - .as_ref()? - .add(EventToUI::Event(out)), - ) + for (_, session) in self.session_handlers.read().unwrap().iter() { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Event(out.clone())); + } + } } - pub(crate) fn close_event_stream(&self) { - let mut stream_lock = self.event_stream.write().unwrap(); - if let Some(stream) = &*stream_lock { - stream.add(EventToUI::Event("close".to_owned())); + pub(crate) fn close_event_stream(&self, session_id: SessionID) { + // to-do: Make sure the following logic is correct. + // No need to remove the display handler, because it will be removed when the connection is closed. + if let Some(session) = self.session_handlers.write().unwrap().get_mut(&session_id) { + try_send_close_event(&session.event_stream); } - *stream_lock = None; } fn make_displays_msg(displays: &Vec) -> String { @@ -314,6 +383,7 @@ impl FlutterHandler { serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) } + #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub(crate) fn add_session_hook(&self, key: String, hook: SessionHook) -> bool { let mut hooks = self.hooks.write().unwrap(); @@ -325,6 +395,7 @@ impl FlutterHandler { true } + #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub(crate) fn remove_session_hook(&self, key: &String) -> bool { let mut hooks = self.hooks.write().unwrap(); @@ -335,27 +406,6 @@ impl FlutterHandler { let _ = hooks.remove(key); true } - - #[inline] - #[cfg(feature = "flutter_texture_render")] - pub fn register_texture(&self, ptr: usize) { - *self.renderer.read().unwrap().ptr.write().unwrap() = ptr; - } - - #[inline] - #[cfg(feature = "flutter_texture_render")] - pub fn set_size(&self, width: usize, height: usize) { - *self.notify_rendered.write().unwrap() = false; - self.renderer.write().unwrap().set_size(width, height); - } - - pub fn on_waiting_for_image_dialog_show(&self) { - #[cfg(any(feature = "flutter_texture_render"))] - { - *self.notify_rendered.write().unwrap() = false; - } - // rgba array render will notify every frame - } } impl InvokeUiSession for FlutterHandler { @@ -408,7 +458,10 @@ impl InvokeUiSession for FlutterHandler { "update_quality_status", vec![ ("speed", &status.speed.map_or(NULL, |it| it)), - ("fps", &status.fps.map_or(NULL, |it| it.to_string())), + ( + "fps", + &serde_json::ser::to_string(&status.fps).unwrap_or(NULL.to_owned()), + ), ("delay", &status.delay.map_or(NULL, |it| it.to_string())), ( "target_bitrate", @@ -529,7 +582,7 @@ impl InvokeUiSession for FlutterHandler { #[inline] #[cfg(not(feature = "flutter_texture_render"))] - fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { + fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) { // Give a chance for plugins or etc to hook a rgba data. #[cfg(not(any(target_os = "android", target_os = "ios")))] for (key, hook) in self.hooks.read().unwrap().iter() { @@ -541,27 +594,51 @@ impl InvokeUiSession for FlutterHandler { } // If the current rgba is not fetched by flutter, i.e., is valid. // We give up sending a new event to flutter. - if self.rgba_valid.load(Ordering::Relaxed) { - return; + let mut rgba_write_lock = self.display_rgbas.write().unwrap(); + if let Some(rgba_data) = rgba_write_lock.get_mut(&display) { + if rgba_data.valid { + return; + } else { + rgba_data.valid = true; + } + // Return the rgba buffer to the video handler for reusing allocated rgba buffer. + std::mem::swap::>(&mut rgba.raw, &mut rgba_data.data); + } else { + let mut rgba_data = RgbaData::default(); + std::mem::swap::>(&mut rgba.raw, &mut rgba_data.data); + rgba_write_lock.insert(display, rgba_data); } - self.rgba_valid.store(true, Ordering::Relaxed); - // Return the rgba buffer to the video handler for reusing allocated rgba buffer. - std::mem::swap::>(&mut rgba.raw, &mut *self.rgba.write().unwrap()); - if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Rgba); + drop(rgba_write_lock); + + // Non-texture-render UI does not support multiple displays in the one UI session. + // It's Ok to notify each session for now. + for h in self.session_handlers.read().unwrap().values() { + if let Some(stream) = &h.event_stream { + stream.add(EventToUI::Rgba(display)); + } } } #[inline] #[cfg(feature = "flutter_texture_render")] - fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { - self.renderer.read().unwrap().on_rgba(rgba); - if *self.notify_rendered.read().unwrap() { - return; + fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb) { + let mut try_notify_sessions = Vec::new(); + for (id, session) in self.session_handlers.read().unwrap().iter() { + session.renderer.on_rgba(display, rgba); + if !session.notify_rendered { + try_notify_sessions.push(id.clone()); + } } - if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Rgba); - *self.notify_rendered.write().unwrap() = true; + if try_notify_sessions.len() > 0 { + let mut write_lock = self.session_handlers.write().unwrap(); + for id in try_notify_sessions.iter() { + if let Some(session) = write_lock.get_mut(id) { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Rgba(display)); + session.notify_rendered = true; + } + } + } } } @@ -578,6 +655,17 @@ impl InvokeUiSession for FlutterHandler { let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); let resolutions = serialize_resolutions(&pi.resolutions.resolutions); *self.peer_info.write().unwrap() = pi.clone(); + #[cfg(feature = "flutter_texture_render")] + { + self.session_handlers + .write() + .unwrap() + .values_mut() + .for_each(|h| { + h.renderer.is_support_multi_ui_session = + crate::common::is_support_multi_ui_session(&pi.version); + }); + } self.push_event( "peer_info", vec![ @@ -701,21 +789,31 @@ impl InvokeUiSession for FlutterHandler { } #[inline] - fn get_rgba(&self) -> *const u8 { + fn get_rgba(&self, _display: usize) -> *const u8 { #[cfg(not(feature = "flutter_texture_render"))] - if self.rgba_valid.load(Ordering::Relaxed) { - return self.rgba.read().unwrap().as_ptr(); + if let Some(rgba_data) = self.display_rgbas.read().unwrap().get(&_display) { + if rgba_data.valid { + return rgba_data.data.as_ptr(); + } } std::ptr::null_mut() } #[inline] - fn next_rgba(&self) { + fn next_rgba(&self, _display: usize) { #[cfg(not(feature = "flutter_texture_render"))] - self.rgba_valid.store(false, Ordering::Relaxed); + if let Some(rgba_data) = self.display_rgbas.write().unwrap().get_mut(&_display) { + rgba_data.valid = true; + } } } +// This function is only used for the default connection session. +pub fn session_add_existed(peer_id: String, session_id: SessionID) -> ResultType<()> { + sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id); + Ok(()) +} + /// Create a new remote session with the given id. /// /// # Arguments @@ -733,18 +831,6 @@ pub fn session_add( force_relay: bool, password: String, ) -> ResultType { - LocalConfig::set_remote_id(&id); - - let session: Session = Session { - session_id: session_id.clone(), - id: id.to_owned(), - password, - server_keyboard_enabled: Arc::new(RwLock::new(true)), - server_file_transfer_enabled: Arc::new(RwLock::new(true)), - server_clipboard_enabled: Arc::new(RwLock::new(true)), - ..Default::default() - }; - let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER } else if is_port_forward { @@ -757,6 +843,26 @@ pub fn session_add( ConnType::DEFAULT_CONN }; + // to-do: check the same id session. + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + if session.lc.read().unwrap().conn_type != conn_type { + bail!("same session id is found with different conn type?"); + } + // The same session is added before? + bail!("same session id is found"); + } + + LocalConfig::set_remote_id(&id); + + let session: Session = Session { + id: id.to_owned(), + password, + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), + ..Default::default() + }; + let switch_uuid = if switch_uuid.is_empty() { None } else { @@ -768,12 +874,8 @@ pub fn session_add( .write() .unwrap() .initialize(id.to_owned(), conn_type, switch_uuid, force_relay); - let session = Arc::new(session.clone()); - if let Some(same_id_session) = sessions::add_session(session_id.to_owned(), session.clone()) { - log::error!("Should not happen"); - same_id_session.close(); - } + sessions::insert_session(session_id.to_owned(), conn_type, session.clone()); Ok(session) } @@ -789,18 +891,39 @@ pub fn session_start_( id: &str, event_stream: StreamSink, ) -> ResultType<()> { - if let Some(session) = sessions::get_session(session_id) { - #[cfg(feature = "flutter_texture_render")] - log::info!( - "Session {} start, render by flutter texture rgba plugin", - id + // is_connected is used to indicate whether to start a peer connection. For two cases: + // 1. "Move tab to new window" + // 2. multi ui session within the same peer connnection. + let mut is_connected = false; + let mut is_found = false; + for s in sessions::get_sessions() { + if let Some(h) = s.session_handlers.write().unwrap().get_mut(session_id) { + is_connected = h.event_stream.is_some(); + try_send_close_event(&h.event_stream); + h.event_stream = Some(event_stream); + is_found = true; + break; + } + } + if !is_found { + bail!( + "No session with peer id {}, session id: {}", + id, + session_id.to_string() ); - #[cfg(not(feature = "flutter_texture_render"))] - log::info!("Session {} start, render by flutter paint widget", id); - let is_pre_added = session.event_stream.read().unwrap().is_some(); - session.close_event_stream(); - *session.event_stream.write().unwrap() = Some(event_stream); - if !is_pre_added { + } + + if let Some(session) = sessions::get_session_by_session_id(session_id) { + let is_first_ui_session = session.session_handlers.read().unwrap().len() == 1; + if !is_connected && is_first_ui_session { + #[cfg(feature = "flutter_texture_render")] + log::info!( + "Session {} start, render by flutter texture rgba plugin", + id + ); + #[cfg(not(feature = "flutter_texture_render"))] + log::info!("Session {} start, render by flutter paint widget", id); + let session = (*session).clone(); std::thread::spawn(move || { let round = session.connection_round_state.lock().unwrap().new_round(); @@ -813,19 +936,26 @@ pub fn session_start_( } } +#[inline] +fn try_send_close_event(event_stream: &Option>) { + if let Some(stream) = &event_stream { + stream.add(EventToUI::Event("close".to_owned())); + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_text_clipboard_required() { let is_required = sessions::get_sessions() .iter() - .any(|session| session.is_text_clipboard_required()); + .any(|s| s.is_text_clipboard_required()); Client::set_is_text_clipboard_required(is_required); } #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn send_text_clipboard_msg(msg: Message) { - for session in sessions::get_sessions() { - if session.is_text_clipboard_required() { - session.send(Data::Message(msg.clone())); + for s in sessions::get_sessions() { + if s.is_text_clipboard_required() { + s.send(Data::Message(msg.clone())); } } } @@ -998,6 +1128,11 @@ pub fn get_cur_session_id() -> SessionID { CUR_SESSION_ID.read().unwrap().clone() } +pub fn get_cur_peer_id() -> String { + sessions::get_peer_id_by_session_id(&get_cur_session_id(), ConnType::DEFAULT_CONN) + .unwrap_or("".to_string()) +} + pub fn set_cur_session_id(session_id: SessionID) { if get_cur_session_id() != session_id { *CUR_SESSION_ID.write().unwrap() = session_id; @@ -1034,47 +1169,76 @@ fn char_to_session_id(c: *const char) -> ResultType { SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e)) } -pub fn session_get_rgba_size(_session_id: SessionID) -> usize { +pub fn session_get_rgba_size(_session_id: SessionID, _display: usize) -> usize { #[cfg(not(feature = "flutter_texture_render"))] - if let Some(session) = sessions::get_session(&_session_id) { - return session.rgba.read().unwrap().len(); + if let Some(session) = sessions::get_session_by_session_id(&_session_id) { + return session + .display_rgbas + .read() + .unwrap() + .get(&_display) + .map_or(0, |rgba| rgba.data.len()); } 0 } #[no_mangle] -pub extern "C" fn session_get_rgba(session_uuid_str: *const char) -> *const u8 { +pub extern "C" fn session_get_rgba(session_uuid_str: *const char, display: usize) -> *const u8 { if let Ok(session_id) = char_to_session_id(session_uuid_str) { - if let Some(session) = sessions::get_session(&session_id) { - return session.get_rgba(); + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + return s.ui_handler.get_rgba(display); } } std::ptr::null() } -pub fn session_next_rgba(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { - return session.next_rgba(); +pub fn session_next_rgba(session_id: SessionID, display: usize) { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + return s.ui_handler.next_rgba(display); } } #[inline] -pub fn session_register_texture(_session_id: SessionID, _ptr: usize) { +pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, _height: usize) { #[cfg(feature = "flutter_texture_render")] - if let Some(session) = sessions::get_session(&_session_id) { - session.register_texture(_ptr); - return; + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .write() + .unwrap() + .get_mut(&_session_id) + { + h.notify_rendered = false; + h.renderer.set_size(_display, _width, _height); + break; + } } } #[inline] -pub fn push_session_event( - session_id: &SessionID, - name: &str, - event: Vec<(&str, &str)>, -) -> Option { - sessions::get_session(session_id)?.push_event(name, event) +pub fn session_register_texture(_session_id: SessionID, _display: usize, _ptr: usize) { + #[cfg(feature = "flutter_texture_render")] + for s in sessions::get_sessions() { + if let Some(h) = s + .ui_handler + .session_handlers + .read() + .unwrap() + .get(&_session_id) + { + h.renderer.register_texture(_display, _ptr); + break; + } + } +} + +#[inline] +pub fn push_session_event(session_id: &SessionID, name: &str, event: Vec<(&str, &str)>) { + if let Some(s) = sessions::get_session_by_session_id(session_id) { + s.push_event(name, event); + } } #[inline] @@ -1123,7 +1287,7 @@ fn session_send_touch_scale( ) { match v.get("v").and_then(|s| s.as_i64()) { Some(scale) => { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_touch_scale(scale as _, alt, ctrl, shift, command); } } @@ -1147,7 +1311,7 @@ fn session_send_touch_pan( v.get("y").and_then(|y| y.as_i64()), ) { (Some(x), Some(y)) => { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session .send_touch_pan_event(pan_event, x as _, y as _, alt, ctrl, shift, command); } @@ -1191,6 +1355,15 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) { } } +#[inline] +pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { + for s in sessions::get_sessions() { + if let Some(h) = s.session_handlers.write().unwrap().get_mut(&session_id) { + h.on_waiting_for_image_dialog_show(); + } + } +} + /// Hooks for session. #[derive(Clone)] pub enum SessionHook { @@ -1199,7 +1372,7 @@ pub enum SessionHook { #[inline] pub fn get_cur_session() -> Option { - sessions::get_session(&*CUR_SESSION_ID.read().unwrap()) + sessions::get_session_by_session_id(&*CUR_SESSION_ID.read().unwrap()) } // sessions mod is used to avoid the big lock of sessions' map. @@ -1207,22 +1380,118 @@ pub mod sessions { use super::*; lazy_static::lazy_static! { - static ref SESSIONS: RwLock> = Default::default(); + // peer -> peer session, peer session -> ui sessions + static ref SESSIONS: RwLock> = Default::default(); } #[inline] - pub fn add_session(session_id: SessionID, session: FlutterSession) -> Option { - SESSIONS.write().unwrap().insert(session_id, session) + pub fn get_session_count(peer_id: String, conn_type: ConnType) -> usize { + SESSIONS + .read() + .unwrap() + .get(&(peer_id, conn_type)) + .map(|s| s.ui_handler.session_handlers.read().unwrap().len()) + .unwrap_or(0) } #[inline] - pub fn remove_session(session_id: &SessionID) -> Option { - SESSIONS.write().unwrap().remove(session_id) + pub fn get_peer_id_by_session_id(id: &SessionID, conn_type: ConnType) -> Option { + SESSIONS + .read() + .unwrap() + .iter() + .find_map(|((peer_id, t), s)| { + if *t == conn_type + && s.ui_handler + .session_handlers + .read() + .unwrap() + .contains_key(id) + { + Some(peer_id.clone()) + } else { + None + } + }) } #[inline] - pub fn get_session(session_id: &SessionID) -> Option { - SESSIONS.read().unwrap().get(session_id).cloned() + pub fn get_session_by_session_id(id: &SessionID) -> Option { + SESSIONS + .read() + .unwrap() + .values() + .find(|s| { + s.ui_handler + .session_handlers + .read() + .unwrap() + .contains_key(id) + }) + .cloned() + } + + #[inline] + pub fn get_session_by_peer_id(peer_id: String, conn_type: ConnType) -> Option { + SESSIONS.read().unwrap().get(&(peer_id, conn_type)).cloned() + } + + #[inline] + pub fn remove_session_by_session_id(id: &SessionID) -> Option { + let mut remove_peer_key = None; + for (peer_key, s) in SESSIONS.write().unwrap().iter_mut() { + let mut write_lock = s.ui_handler.session_handlers.write().unwrap(); + if write_lock.remove(id).is_some() { + if write_lock.is_empty() { + remove_peer_key = Some(peer_key.clone()); + } + break; + } + } + SESSIONS.write().unwrap().remove(&remove_peer_key?) + } + + #[inline] + pub fn insert_session(session_id: SessionID, conn_type: ConnType, session: FlutterSession) { + SESSIONS + .write() + .unwrap() + .entry((session.id.clone(), conn_type)) + .or_insert(session) + .ui_handler + .session_handlers + .write() + .unwrap() + .insert(session_id, Default::default()); + } + + #[inline] + pub fn insert_peer_session_id( + peer_id: String, + conn_type: ConnType, + session_id: SessionID, + ) -> bool { + if let Some(s) = SESSIONS.read().unwrap().get(&(peer_id, conn_type)) { + #[cfg(not(feature = "flutter_texture_render"))] + let h = SessionHandler::default(); + #[cfg(feature = "flutter_texture_render")] + let mut h = SessionHandler::default(); + #[cfg(feature = "flutter_texture_render")] + { + h.renderer.is_support_multi_ui_session = crate::common::is_support_multi_ui_session( + &s.ui_handler.peer_info.read().unwrap().version, + ); + } + let _ = s + .ui_handler + .session_handlers + .write() + .unwrap() + .insert(session_id, h); + true + } else { + false + } } #[inline] @@ -1230,26 +1499,14 @@ pub mod sessions { SESSIONS.read().unwrap().values().cloned().collect() } - #[inline] - pub fn get_session_by_peer_id(peer_id: &str) -> Option { - SESSIONS - .read() - .unwrap() - .values() - .find(|session| session.id == peer_id) - .map(|s| s.clone()) - .clone() - } - #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn other_sessions_running(session_id: &SessionID) -> bool { + pub fn other_sessions_running(peer_id: String, conn_type: ConnType) -> bool { SESSIONS .read() .unwrap() - .keys() - .filter(|k| *k != session_id) - .count() - != 0 + .get(&(peer_id, conn_type)) + .map(|s| s.session_handlers.read().unwrap().len() != 0) + .unwrap_or(false) } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4ae89bc9..080ad8178 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -4,7 +4,7 @@ use crate::{ client::file_trait::FileManager, common::is_keyboard_mode_supported, common::make_fd_to_json, - flutter::{self, session_add, session_start_, sessions}, + flutter::{self, session_add, session_add_existed, session_start_, sessions}, input::*, ui_interface::{self, *}, }; @@ -16,6 +16,7 @@ use hbb_common::{ config::{self, LocalConfig, PeerConfig, PeerInfoSerde}, fs, lazy_static, log, message_proto::KeyboardMode, + rendezvous_proto::ConnType, ResultType, }; use std::{ @@ -67,7 +68,7 @@ pub fn stop_global_event_stream(app_type: String) { } pub enum EventToUI { Event(String), - Rgba, + Rgba(usize), } pub fn host_stop_system_key_propagate(_stopped: bool) { @@ -75,8 +76,26 @@ pub fn host_stop_system_key_propagate(_stopped: bool) { crate::platform::windows::stop_system_key_propagate(_stopped); } -// FIXME: -> ResultType<()> cannot be parsed by frb_codegen -// thread 'main' panicked at 'Failed to parse function output type `ResultType<()>`', $HOME\.cargo\git\checkouts\flutter_rust_bridge-ddba876d3ebb2a1e\e5adce5\frb_codegen\src\parser\mod.rs:151:25 +// This function is only used to count the number of control sessions. +pub fn peer_get_default_sessions_count(id: String) -> SyncReturn { + SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN)) +} + +pub fn session_add_existed_sync(id: String, session_id: SessionID) -> SyncReturn { + if let Err(e) = session_add_existed(id.clone(), session_id) { + SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) + } else { + SyncReturn("".to_owned()) + } +} + +pub fn session_try_add_display(session_id: SessionID, displays: Vec) -> SyncReturn<()> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.capture_displays(displays, vec![], vec![]); + } + SyncReturn(()) +} + pub fn session_add_sync( session_id: SessionID, id: String, @@ -112,7 +131,7 @@ pub fn session_start( } pub fn session_get_remember(session_id: SessionID) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_remember()) } else { None @@ -120,7 +139,7 @@ pub fn session_get_remember(session_id: SessionID) -> Option { } pub fn session_get_toggle_option(session_id: SessionID, arg: String) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_toggle_option(arg)) } else { None @@ -133,7 +152,7 @@ pub fn session_get_toggle_option_sync(session_id: SessionID, arg: String) -> Syn } pub fn session_get_option(session_id: SessionID, arg: String) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_option(arg)) } else { None @@ -147,55 +166,61 @@ pub fn session_login( password: String, remember: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.login(os_username, os_password, password, remember); } } pub fn session_close(session_id: SessionID) { - if let Some(session) = sessions::remove_session(&session_id) { - session.close_event_stream(); + if let Some(session) = sessions::remove_session_by_session_id(&session_id) { + session.close_event_stream(session_id); session.close(); } } -pub fn session_refresh(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { - session.refresh_video(); +pub fn session_refresh(session_id: SessionID, display: usize) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.refresh_video(display); } } -pub fn session_record_screen(session_id: SessionID, start: bool, width: usize, height: usize) { - if let Some(session) = sessions::get_session(&session_id) { - session.record_screen(start, width as _, height as _); +pub fn session_record_screen( + session_id: SessionID, + start: bool, + display: usize, + width: usize, + height: usize, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.record_screen(start, display as _, width as _, height as _); } } pub fn session_record_status(session_id: SessionID, status: bool) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.record_status(status); } } pub fn session_reconnect(session_id: SessionID, force_relay: bool) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.reconnect(force_relay); } } pub fn session_toggle_option(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { log::warn!("toggle option {}", &value); session.toggle_option(value.clone()); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if sessions::get_session(&session_id).is_some() && value == "disable-clipboard" { + if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } } pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_flutter_option(k)) } else { None @@ -203,13 +228,14 @@ pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option Option { - if let Some(session) = sessions::get_session_by_peer_id(&id) { + if let Some(session) = sessions::get_session_by_peer_id(id, ConnType::DEFAULT_CONN) { Some(session.get_flutter_option(k)) } else { None @@ -238,7 +264,7 @@ pub fn set_local_kb_layout_type(kb_layout_type: String) { } pub fn session_get_view_style(session_id: SessionID) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_view_style()) } else { None @@ -246,13 +272,13 @@ pub fn session_get_view_style(session_id: SessionID) -> Option { } pub fn session_set_view_style(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.save_view_style(value); } } pub fn session_get_scroll_style(session_id: SessionID) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_scroll_style()) } else { None @@ -260,13 +286,13 @@ pub fn session_get_scroll_style(session_id: SessionID) -> Option { } pub fn session_set_scroll_style(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.save_scroll_style(value); } } pub fn session_get_image_quality(session_id: SessionID) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_image_quality()) } else { None @@ -274,13 +300,13 @@ pub fn session_get_image_quality(session_id: SessionID) -> Option { } pub fn session_set_image_quality(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.save_image_quality(value); } } pub fn session_get_keyboard_mode(session_id: SessionID) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_keyboard_mode()) } else { None @@ -289,7 +315,7 @@ pub fn session_get_keyboard_mode(session_id: SessionID) -> Option { pub fn session_set_keyboard_mode(session_id: SessionID, value: String) { let mut _mode_updated = false; - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.save_keyboard_mode(value.clone()); _mode_updated = true; } @@ -300,7 +326,7 @@ pub fn session_set_keyboard_mode(session_id: SessionID, value: String) { } pub fn session_get_reverse_mouse_wheel(session_id: SessionID) -> Option { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_reverse_mouse_wheel()) } else { None @@ -308,13 +334,13 @@ pub fn session_get_reverse_mouse_wheel(session_id: SessionID) -> Option } pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.save_reverse_mouse_wheel(value); } } pub fn session_get_custom_image_quality(session_id: SessionID) -> Option> { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_custom_image_quality()) } else { None @@ -322,7 +348,7 @@ pub fn session_get_custom_image_quality(session_id: SessionID) -> Option SyncReturn { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { SyncReturn(is_keyboard_mode_supported( &mode, @@ -337,32 +363,36 @@ pub fn session_is_keyboard_mode_supported(session_id: SessionID, mode: String) - } pub fn session_set_custom_image_quality(session_id: SessionID, value: i32) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.save_custom_image_quality(value); } } pub fn session_set_custom_fps(session_id: SessionID, fps: i32) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.set_custom_fps(fps); } } pub fn session_lock_screen(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.lock_screen(); } } pub fn session_ctrl_alt_del(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.ctrl_alt_del(); } } -pub fn session_switch_display(session_id: SessionID, value: i32) { - if let Some(session) = sessions::get_session(&session_id) { - session.switch_display(value); +pub fn session_switch_display(session_id: SessionID, value: Vec) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + if value.len() == 1 { + session.switch_display(value[0]); + } else { + session.capture_displays(vec![], vec![], value); + } } } @@ -374,7 +404,7 @@ pub fn session_handle_flutter_key_event( lock_modes: i32, down_or_up: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { let keyboard_mode = session.get_keyboard_mode(); session.handle_flutter_key_event( &keyboard_mode, @@ -395,7 +425,7 @@ pub fn session_handle_flutter_key_event( // This will cause the keyboard input to take no effect. pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> { #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(session) = sessions::get_session(&_session_id) { + if let Some(session) = sessions::get_session_by_session_id(&_session_id) { let keyboard_mode = session.get_keyboard_mode(); if _enter { set_cur_session_id_(_session_id, &keyboard_mode); @@ -417,14 +447,14 @@ pub fn session_input_key( shift: bool, command: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_string(&value); } @@ -432,33 +462,33 @@ pub fn session_input_string(session_id: SessionID, value: String) { // chat_client_mode pub fn session_send_chat(session_id: SessionID, text: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_chat(text); } } pub fn session_peer_option(session_id: SessionID, name: String, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.set_option(name, value); } } pub fn session_get_peer_option(session_id: SessionID, name: String) -> String { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { return session.get_option(name); } "".to_string() } pub fn session_input_os_password(session_id: SessionID, value: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.input_os_password(value, true); } } // File Action pub fn session_read_remote_dir(session_id: SessionID, path: String, include_hidden: bool) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.read_remote_dir(path, include_hidden); } } @@ -472,7 +502,7 @@ pub fn session_send_files( include_hidden: bool, is_remote: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_files(act_id, path, to, file_num, include_hidden, is_remote); } } @@ -485,7 +515,7 @@ pub fn session_set_confirm_override_file( remember: bool, is_upload: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload); } } @@ -497,7 +527,7 @@ pub fn session_remove_file( file_num: i32, is_remote: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.remove_file(act_id, path, file_num, is_remote); } } @@ -509,7 +539,7 @@ pub fn session_read_dir_recursive( is_remote: bool, show_hidden: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.remove_dir_all(act_id, path, is_remote, show_hidden); } } @@ -520,19 +550,19 @@ pub fn session_remove_all_empty_dirs( path: String, is_remote: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.remove_dir(act_id, path, is_remote); } } pub fn session_cancel_job(session_id: SessionID, act_id: i32) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.cancel_job(act_id); } } pub fn session_create_dir(session_id: SessionID, act_id: i32, path: String, is_remote: bool) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.create_dir(act_id, path, is_remote); } } @@ -549,14 +579,14 @@ pub fn session_read_local_dir_sync( } pub fn session_get_platform(session_id: SessionID, is_remote: bool) -> String { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { return session.get_platform(is_remote); } "".to_string() } pub fn session_load_last_transfer_jobs(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { return session.load_last_jobs(); } else { // a tip for flutter dev @@ -576,46 +606,44 @@ pub fn session_add_job( include_hidden: bool, is_remote: bool, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.add_job(act_id, path, to, file_num, include_hidden, is_remote); } } pub fn session_resume_job(session_id: SessionID, act_id: i32, is_remote: bool) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.resume_job(act_id, is_remote); } } pub fn session_elevate_direct(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.elevate_direct(); } } pub fn session_elevate_with_logon(session_id: SessionID, username: String, password: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.elevate_with_logon(username, password); } } pub fn session_switch_sides(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.switch_sides(); } } pub fn session_change_resolution(session_id: SessionID, display: i32, width: i32, height: i32) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.change_resolution(display, width, height); } } -pub fn session_set_size(_session_id: SessionID, _width: usize, _height: usize) { +pub fn session_set_size(_session_id: SessionID, _display: usize, _width: usize, _height: usize) { #[cfg(feature = "flutter_texture_render")] - if let Some(session) = sessions::get_session(&_session_id) { - session.set_size(_width, _height); - } + super::flutter::session_set_size(_session_id, _display, _width, _height) } pub fn main_get_sound_inputs() -> Vec { @@ -1008,18 +1036,22 @@ pub fn main_handle_relay_id(id: String) -> String { handle_relay_id(id) } -pub fn main_get_current_display() -> SyncReturn { - #[cfg(not(target_os = "ios"))] - let display_info = match crate::video_service::get_current_display() { - Ok((_, _, display)) => serde_json::to_string(&HashMap::from([ - ("w", display.width()), - ("h", display.height()), - ])) - .unwrap_or_default(), - Err(..) => "".to_string(), - }; +pub fn main_get_main_display() -> SyncReturn { #[cfg(target_os = "ios")] let display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + let mut display_info = "".to_owned(); + #[cfg(not(target_os = "ios"))] + if let Ok(displays) = crate::display_service::try_get_displays() { + // to-do: Need to detect current display index. + if let Some(display) = displays.iter().next() { + display_info = serde_json::to_string(&HashMap::from([ + ("w", display.width()), + ("h", display.height()), + ])) + .unwrap_or_default(); + } + } SyncReturn(display_info) } @@ -1029,31 +1061,31 @@ pub fn session_add_port_forward( remote_host: String, remote_port: i32, ) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.add_port_forward(local_port, remote_host, remote_port); } } pub fn session_remove_port_forward(session_id: SessionID, local_port: i32) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.remove_port_forward(local_port); } } pub fn session_new_rdp(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.new_rdp(); } } pub fn session_request_voice_call(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.request_voice_call(); } } pub fn session_close_voice_call(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.close_voice_call(); } } @@ -1238,20 +1270,20 @@ pub fn session_send_mouse(session_id: SessionID, msg: String) { _ => 0, } << 3; } - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_mouse(mask, x, y, alt, ctrl, shift, command); } } } pub fn session_restart_remote_device(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.restart_remote_device(); } } pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> SyncReturn { - let res = if let Some(session) = sessions::get_session(&session_id) { + let res = if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.get_audit_server(typ) } else { "".to_owned() @@ -1260,13 +1292,13 @@ pub fn session_get_audit_server_sync(session_id: SessionID, typ: String) -> Sync } pub fn session_send_note(session_id: SessionID, note: String) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.send_note(note) } } pub fn session_alternative_codecs(session_id: SessionID) -> String { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { let (vp8, av1, h264, h265) = session.alternative_codecs(); let msg = HashMap::from([("vp8", vp8), ("av1", av1), ("h264", h264), ("h265", h265)]); serde_json::ser::to_string(&msg).unwrap_or("".to_owned()) @@ -1276,15 +1308,13 @@ pub fn session_alternative_codecs(session_id: SessionID) -> String { } pub fn session_change_prefer_codec(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.change_prefer_codec(); } } pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { - session.ui_handler.on_waiting_for_image_dialog_show(); - } + super::flutter::session_on_waiting_for_image_dialog_show(session_id); } pub fn main_set_home_dir(_home: String) { @@ -1434,16 +1464,22 @@ pub fn translate(name: String, locale: String) -> SyncReturn { SyncReturn(crate::client::translate_locale(name, &locale)) } -pub fn session_get_rgba_size(session_id: SessionID) -> SyncReturn { - SyncReturn(super::flutter::session_get_rgba_size(session_id)) +pub fn session_get_rgba_size(session_id: SessionID, display: usize) -> SyncReturn { + SyncReturn(super::flutter::session_get_rgba_size(session_id, display)) } -pub fn session_next_rgba(session_id: SessionID) -> SyncReturn<()> { - SyncReturn(super::flutter::session_next_rgba(session_id)) +pub fn session_next_rgba(session_id: SessionID, display: usize) -> SyncReturn<()> { + SyncReturn(super::flutter::session_next_rgba(session_id, display)) } -pub fn session_register_texture(session_id: SessionID, ptr: usize) -> SyncReturn<()> { - SyncReturn(super::flutter::session_register_texture(session_id, ptr)) +pub fn session_register_texture( + session_id: SessionID, + display: usize, + ptr: usize, +) -> SyncReturn<()> { + SyncReturn(super::flutter::session_register_texture( + session_id, display, ptr, + )) } pub fn query_onlines(ids: Vec) { @@ -1522,7 +1558,7 @@ pub fn main_update_me() -> SyncReturn { } pub fn set_cur_session_id(session_id: SessionID) { - if let Some(session) = sessions::get_session(&session_id) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { set_cur_session_id_(session_id, &session.get_keyboard_mode()) } } @@ -1808,6 +1844,10 @@ pub fn plugin_install(_id: String, _b: bool) { } } +pub fn is_support_multi_ui_session(version: String) -> SyncReturn { + SyncReturn(crate::common::is_support_multi_ui_session(&version)) +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 7921dbde4..30c8bea5f 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 61cbef359..d9c570796 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index ea8f4ee4e..24d2ec78a 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", "按交集过滤"), ("Remove wallpaper during incoming sessions", "接受会话时移除桌面壁纸"), ("Test", "测试"), + ("switch_display_elevated_connections_tip", "提权后,被控有多个连接,不能切换到非主显示器。若要控制多显示器,请安装后再试。"), + ("display_is_plugged_out_msg", "显示器被拔出,切换到第一个显示器。"), + ("No displays", "没有显示器。"), + ("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"), + ("Choose Display Behavior", "选择显示器的行为"), + ("Switch Display", "切换显示器"), + ("Open in New Window", "在新的窗口中打开"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ff0b87502..4f54e0540 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 6b577e5b8..17792e10b 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index aac437388..0628846d3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -557,6 +557,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), - ("Filter by intersection", "Nach Schnittpunkt filtern") + ("Filter by intersection", "Nach Schnittpunkt filtern"), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 169ee2359..fa545e182 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index ea5c18399..e2495780e 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -222,5 +222,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_rustdesk_server_pro_to_{}_tip", "Please upgrade RustDesk Server Pro to version {} or newer!"), ("pull_group_failed_tip", "Failed to refresh group"), ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("switch_display_elevated_connections_tip", "Switching to non-primary display is not supported in the elevated mode when there are multiple connections. Please try again after installation if you want to control multiple displays."), + ("display_is_plugged_out_msg", "The diplay is plugged out, switch to the first display."), + ("elevated_switch_display_msg", "Switch to the primary display because multiple display is not supported in elevated mode."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 59c4db9fe..56f24354c 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index c8abc4e1b..46b0dc63c 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index e5b20d39c..802b6473c 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 075167af1..bec161910 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3c3e50205..9fb02464e 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index d162971b9..ac6b1276d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index d21870c26..8d6f94b00 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -559,5 +559,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", "Filtra per incrocio"), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 61dafdb34..7bad54921 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index f4c9076ca..5d0b7eab7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 20efc25c4..24f97be4b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index c360d3ecb..b35f33498 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9d72e407a..ffa33d061 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", "Filtrēt pēc krustpunkta"), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 3b30015c3..f67b3dd23 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 77b3204b8..81c23ef84 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 93d20600b..360cfd4fa 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 62e2ef812..8df3e9ea8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 6d755a802..1822d79f2 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 22e920b8e..a0be18469 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", "Фильтровать по пересечению"), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 3f0ad7bb3..75bb447f4 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 4d05f7e1b..5f9f8d5f3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ce69547d3..3fef0845d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index d1e5cd206..3382ac2b6 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 7deaac905..eca0c4f19 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f3187966e..256bb7d76 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 2b2f63d34..5477cb666 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 69de33892..c8d323d8a 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 15c404937..ad37066f3 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index d3b3b9181..7b510a224 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index e29da9ae0..309d34f7a 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -558,5 +558,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Filter by intersection", ""), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), + ("switch_display_elevated_connections_tip", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("elevated_switch_display_msg", ""), + ("Choose Display Behavior", ""), + ("Switch Display", ""), + ("Open in New Window", ""), ].iter().cloned().collect(); } diff --git a/src/server.rs b/src/server.rs index c296e60be..0e1abfd55 100644 --- a/src/server.rs +++ b/src/server.rs @@ -26,7 +26,7 @@ use hbb_common::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; -use service::{GenericService, Service, Subscriber}; +use service::{EmptyExtraFieldService, GenericService, Service, Subscriber}; use crate::ipc::Data; @@ -53,6 +53,7 @@ pub const NAME_POS: &'static str = ""; } mod connection; +pub mod display_service; #[cfg(windows)] pub mod portable_service; mod service; @@ -80,7 +81,7 @@ lazy_static::lazy_static! { pub struct Server { connections: ConnMap, - services: HashMap<&'static str, Box>, + services: HashMap>, id_count: i32, } @@ -94,11 +95,15 @@ pub fn new() -> ServerPtr { id_count: hbb_common::rand::random::() % 1000 + 1000, // ensure positive }; server.add_service(Box::new(audio_service::new())); - server.add_service(Box::new(video_service::new())); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + server.add_service(Box::new(display_service::new())); + server.add_service(Box::new(video_service::new( + *display_service::PRIMARY_DISPLAY_IDX, + ))); #[cfg(not(any(target_os = "android", target_os = "ios")))] { server.add_service(Box::new(clipboard_service::new())); - if !video_service::capture_cursor_embedded() { + if !display_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); } @@ -253,9 +258,19 @@ async fn create_relay_connection_( } impl Server { + fn is_video_service_name(name: &str) -> bool { + name.starts_with(video_service::NAME) + } + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { + let primary_video_service_name = + video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); for s in self.services.values() { - if !noperms.contains(&s.name()) { + let name = s.name(); + if Self::is_video_service_name(&name) && name != primary_video_service_name { + continue; + } + if !noperms.contains(&(&name as _)) { s.on_subscribe(conn.clone()); } } @@ -287,8 +302,12 @@ impl Server { self.services.insert(name, service); } + pub fn contains(&self, name: &str) -> bool { + self.services.contains_key(name) + } + pub fn subscribe(&mut self, name: &str, conn: ConnInner, sub: bool) { - if let Some(s) = self.services.get(&name) { + if let Some(s) = self.services.get(name) { if s.is_subed(conn.id()) == sub { return; } @@ -305,6 +324,47 @@ impl Server { self.id_count += 1; self.id_count } + + pub fn set_video_service_opt(&self, display: Option, opt: &str, value: &str) { + for (k, v) in self.services.iter() { + if let Some(display) = display { + if k != &video_service::get_service_name(display) { + continue; + } + } + + if Self::is_video_service_name(k) { + v.set_option(opt, value); + } + } + } + + fn capture_displays( + &mut self, + conn: ConnInner, + displays: &[usize], + include: bool, + exclude: bool, + ) { + let displays = displays + .iter() + .map(|d| video_service::get_service_name(*d)) + .collect::>(); + let keys = self.services.keys().cloned().collect::>(); + for name in keys.iter() { + if Self::is_video_service_name(&name) { + if displays.contains(&name) { + if include { + self.subscribe(&name, conn.clone(), true); + } + } else { + if exclude { + self.subscribe(&name, conn.clone(), false); + } + } + } + } + } } impl Drop for Server { diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index ac3cd6b95..98aa7fa31 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -24,16 +24,16 @@ static RESTARTING: AtomicBool = AtomicBool::new(false); #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn new() -> GenericService { - let sp = GenericService::new(NAME, true); - sp.repeat::(33, cpal_impl::run); - sp + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::repeat::(&svc.clone(), 33, cpal_impl::run); + svc.sp } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn new() -> GenericService { - let sp = GenericService::new(NAME, true); - sp.run(pa_impl::run); - sp + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::run(&svc.clone(), pa_impl::run); + svc.sp } pub fn restart() { @@ -48,7 +48,7 @@ pub fn restart() { mod pa_impl { use super::*; #[tokio::main(flavor = "current_thread")] - pub async fn run(sp: GenericService) -> ResultType<()> { + pub async fn run(sp: EmptyExtraFieldService) -> ResultType<()> { hbb_common::sleep(0.1).await; // one moment to wait for _pa ipc RESTARTING.store(false, Ordering::SeqCst); #[cfg(target_os = "linux")] @@ -125,7 +125,7 @@ mod cpal_impl { } } - pub fn run(sp: GenericService, state: &mut State) -> ResultType<()> { + pub fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { sp.snapshot(|sps| { match &state.stream { None => { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 25597d11c..be521b152 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -28,12 +28,12 @@ impl super::service::Reset for State { } pub fn new() -> GenericService { - let sp = GenericService::new(NAME, true); - sp.repeat::(INTERVAL, run); - sp + let svc = EmptyExtraFieldService::new(NAME.to_owned(), true); + GenericService::repeat::(&svc.clone(), INTERVAL, run); + svc.sp } -fn run(sp: GenericService, state: &mut State) -> ResultType<()> { +fn run(sp: EmptyExtraFieldService, state: &mut State) -> ResultType<()> { if let Some(ctx) = state.ctx.as_mut() { if let Some(msg) = check_clipboard(ctx, None) { sp.send(msg); diff --git a/src/server/connection.rs b/src/server/connection.rs index fa2b8261b..990ee733c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -15,7 +15,7 @@ use crate::{ new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, }, common::{get_default_sound_input, set_sound_input}, - video_service, + display_service, video_service, }; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; @@ -160,6 +160,7 @@ pub enum AuthConnType { pub struct Connection { inner: ConnInner, + display_idx: usize, stream: super::Stream, server: super::ServerPtrWeak, hash: Hash, @@ -303,6 +304,7 @@ impl Connection { tx: Some(tx), tx_video: Some(tx_video), }, + display_idx: *display_service::PRIMARY_DISPLAY_IDX, stream, server, hash, @@ -609,6 +611,9 @@ impl Connection { _ => {}, } } + Some(message::Union::PeerInfo(_)) => { + conn.refresh_video_display(None); + } _ => {} } if let Err(err) = conn.stream.send(msg).await { @@ -1098,19 +1103,20 @@ impl Connection { pi.username = username; pi.sas_enabled = sas_enabled; pi.features = Some(Features { - privacy_mode: video_service::is_privacy_mode_supported(), + privacy_mode: display_service::is_privacy_mode_supported(), ..Default::default() }) .into(); - // `try_reset_current_display` is needed because `get_displays` may change the current display, - // which may cause the mismatch of current display and the current display name. - #[cfg(not(any(target_os = "android", target_os = "ios")))] - video_service::try_reset_current_display(); #[cfg(not(any(target_os = "android", target_os = "ios")))] { pi.resolutions = Some(SupportedResolutions { - resolutions: video_service::get_current_display() - .map(|(_, _, d)| crate::platform::resolutions(&d.name())) + resolutions: display_service::try_get_displays() + .map(|displays| { + displays + .get(self.display_idx) + .map(|d| crate::platform::resolutions(&d.name())) + .unwrap_or(vec![]) + }) .unwrap_or(vec![]), ..Default::default() }) @@ -1126,16 +1132,15 @@ impl Connection { self.send(msg_out).await; } - match super::video_service::get_displays().await { + match super::display_service::get_displays().await { Err(err) => { res.set_error(format!("{}", err)); } - Ok((current, displays)) => { + Ok(displays) => { pi.displays = displays.clone(); - pi.current_display = current as _; + pi.current_display = self.display_idx as _; res.set_peer_info(pi); sub_service = true; - *super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays; } } Self::on_remote_authorized(); @@ -1289,6 +1294,8 @@ impl Connection { #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] fn input_key(&self, msg: KeyEvent, press: bool) { + // to-do: if is the legacy mode, and the key is function key "LockScreen". + // Switch to the primary display. self.tx_input.send(MessageInput::Key((msg, press))).ok(); } @@ -1945,15 +1952,13 @@ impl Connection { }, Some(message::Union::Misc(misc)) => match misc.union { Some(misc::Union::SwitchDisplay(s)) => { - video_service::switch_display(s.display).await; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if s.width != 0 && s.height != 0 { - self.change_resolution(&Resolution { - width: s.width, - height: s.height, - ..Default::default() - }); - } + self.handle_switch_display(s).await; + } + Some(misc::Union::CaptureDisplays(displays)) => { + let add = displays.add.iter().map(|d| *d as usize).collect::>(); + let sub = displays.sub.iter().map(|d| *d as usize).collect::>(); + let set = displays.set.iter().map(|d| *d as usize).collect::>(); + self.capture_displays(&add, &sub, &set).await; } Some(misc::Union::ChatMessage(c)) => { self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); @@ -1965,10 +1970,14 @@ impl Connection { } Some(misc::Union::RefreshVideo(r)) => { if r { - super::video_service::refresh(); + self.refresh_video_display(None); } self.update_auto_disconnect_timer(); } + Some(misc::Union::RefreshVideoDisplay(display)) => { + self.refresh_video_display(Some(display as usize)); + self.update_auto_disconnect_timer(); + } Some(misc::Union::VideoReceived(_)) => { video_service::notify_video_frame_fetched( self.inner.id, @@ -2084,6 +2093,75 @@ impl Connection { true } + fn refresh_video_display(&self, display: Option) { + video_service::refresh(); + self.server.upgrade().map(|s| { + s.read().unwrap().set_video_service_opt( + display, + video_service::OPTION_REFRESH, + super::service::SERVICE_OPTION_VALUE_TRUE, + ) + }); + } + + async fn handle_switch_display(&mut self, s: SwitchDisplay) { + #[cfg(windows)] + if portable_client::running() + && *CONN_COUNT.lock().unwrap() > 1 + && s.display != (*display_service::PRIMARY_DISPLAY_IDX as i32) + { + log::info!("Switch to non-primary display is not supported in the elevated mode when there are multiple connections."); + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "nook-nocancel-hasclose".to_owned(), + title: "Prompt".to_owned(), + text: "switch_display_elevated_connections_tip".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + self.send(msg_out).await; + return; + } + + let display_idx = s.display as usize; + if self.display_idx != display_idx { + if let Some(server) = self.server.upgrade() { + self.switch_display_to(display_idx, server.clone()); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if s.width != 0 && s.height != 0 { + self.change_resolution(&Resolution { + width: s.width, + height: s.height, + ..Default::default() + }); + } + + // send display changed message + if let Some(msg_out) = + video_service::make_display_changed_msg(self.display_idx, None) + { + self.send(msg_out).await; + } + } + } + } + + fn switch_display_to(&mut self, display_idx: usize, server: Arc>) { + let new_service_name = video_service::get_service_name(display_idx); + let old_service_name = video_service::get_service_name(self.display_idx); + let mut lock = server.write().unwrap(); + if display_idx != *display_service::PRIMARY_DISPLAY_IDX { + if !lock.contains(&new_service_name) { + lock.add_service(Box::new(video_service::new(display_idx))); + } + } + lock.subscribe(&old_service_name, self.inner.clone(), false); + lock.subscribe(&new_service_name, self.inner.clone(), true); + self.display_idx = display_idx; + } + #[cfg(windows)] async fn handle_elevation_request(&mut self, para: portable_client::StartPara) { let mut err; @@ -2106,36 +2184,64 @@ impl Connection { self.update_auto_disconnect_timer(); } + async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { + if let Some(sever) = self.server.upgrade() { + let mut lock = sever.write().unwrap(); + for display in add.iter() { + let service_name = video_service::get_service_name(*display); + if !lock.contains(&service_name) { + lock.add_service(Box::new(video_service::new(*display))); + } + } + for display in set.iter() { + let service_name = video_service::get_service_name(*display); + if !lock.contains(&service_name) { + lock.add_service(Box::new(video_service::new(*display))); + } + } + if !add.is_empty() { + lock.capture_displays(self.inner.clone(), add, true, false); + } else if !sub.is_empty() { + lock.capture_displays(self.inner.clone(), sub, false, true); + } else { + lock.capture_displays(self.inner.clone(), set, true, true); + } + drop(lock); + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] fn change_resolution(&mut self, r: &Resolution) { if self.keyboard { - if let Ok((_, _, display)) = video_service::get_current_display() { - let name = display.name(); - #[cfg(all(windows, feature = "virtual_display_driver"))] - if let Some(_ok) = - crate::virtual_display_manager::change_resolution_if_is_virtual_display( + if let Ok(displays) = display_service::try_get_displays() { + if let Some(display) = displays.get(self.display_idx) { + let name = display.name(); + #[cfg(all(windows, feature = "virtual_display_driver"))] + if let Some(_ok) = + crate::virtual_display_manager::change_resolution_if_is_virtual_display( + &name, + r.width as _, + r.height as _, + ) + { + return; + } + display_service::set_last_changed_resolution( &name, - r.width as _, - r.height as _, - ) - { - return; - } - video_service::set_last_changed_resolution( - &name, - (display.width() as _, display.height() as _), - (r.width, r.height), - ); - if let Err(e) = - crate::platform::change_resolution(&name, r.width as _, r.height as _) - { - log::error!( - "Failed to change resolution '{}' to ({},{}):{:?}", - &name, - r.width, - r.height, - e + (display.width() as _, display.height() as _), + (r.width, r.height), ); + if let Err(e) = + crate::platform::change_resolution(&name, r.width as _, r.height as _) + { + log::error!( + "Failed to change resolution '{}' to ({},{}):{:?}", + &name, + r.width, + r.height, + e + ); + } } } } @@ -2281,7 +2387,7 @@ impl Connection { if self.keyboard { match q { BoolOption::Yes => { - let msg_out = if !video_service::is_privacy_mode_supported() { + let msg_out = if !display_service::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), @@ -2289,8 +2395,11 @@ impl Connection { } else { match privacy_mode::turn_on_privacy(self.inner.id) { Ok(true) => { - let err_msg = - video_service::test_create_capturer(self.inner.id, 5_000); + let err_msg = video_service::test_create_capturer( + self.inner.id, + self.display_idx, + 5_000, + ); if err_msg.is_empty() { video_service::set_privacy_mode_conn_id(self.inner.id); crate::common::make_privacy_mode_msg( @@ -2326,7 +2435,7 @@ impl Connection { self.send(msg_out).await; } BoolOption::No => { - let msg_out = if !video_service::is_privacy_mode_supported() { + let msg_out = if !display_service::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, "Unsupported. 1 Multi-screen is not supported. 2 Please confirm the license is activated.".to_string(), @@ -2801,11 +2910,11 @@ mod raii { active_conns_lock.retain(|&c| c != self.0); #[cfg(not(any(target_os = "android", target_os = "ios")))] if active_conns_lock.is_empty() { - video_service::reset_resolutions(); + display_service::reset_resolutions(); } #[cfg(all(windows, feature = "virtual_display_driver"))] if active_conns_lock.is_empty() { - video_service::try_plug_out_virtual_display(); + display_service::try_plug_out_virtual_display(); } #[cfg(all(windows))] if active_conns_lock.is_empty() { diff --git a/src/server/display_service.rs b/src/server/display_service.rs new file mode 100644 index 000000000..dd4410312 --- /dev/null +++ b/src/server/display_service.rs @@ -0,0 +1,262 @@ +use super::*; +#[cfg(all(windows, feature = "virtual_display_driver"))] +use crate::virtual_display_manager; +#[cfg(windows)] +use hbb_common::get_version_number; +use hbb_common::protobuf::MessageField; +use scrap::Display; + +pub const NAME: &'static str = "display"; + +struct ChangedResolution { + original: (i32, i32), + changed: (i32, i32), +} + +lazy_static::lazy_static! { + static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported(); + static ref CHANGED_RESOLUTIONS: Arc>> = Default::default(); + // Initial primary display index. + // It should only be updated when the rustdesk server is started, and should not be updated when displays changed. + pub static ref PRIMARY_DISPLAY_IDX: usize = get_primary(); +} + +#[inline] +pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) { + let mut lock = CHANGED_RESOLUTIONS.write().unwrap(); + match lock.get_mut(display_name) { + Some(res) => res.changed = changed, + None => { + lock.insert( + display_name.to_owned(), + ChangedResolution { original, changed }, + ); + } + } +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn reset_resolutions() { + for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() { + let (w, h) = res.original; + if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) { + log::error!( + "Failed to reset resolution of display '{}' to ({},{}): {}", + name, + w, + h, + e + ); + } + } +} + +#[inline] +fn is_capturer_mag_supported() -> bool { + #[cfg(windows)] + return scrap::CapturerMag::is_supported(); + #[cfg(not(windows))] + false +} + +#[inline] +pub fn capture_cursor_embedded() -> bool { + scrap::is_cursor_embedded() +} + +#[inline] +pub fn is_privacy_mode_supported() -> bool { + #[cfg(windows)] + return *IS_CAPTURER_MAGNIFIER_SUPPORTED + && get_version_number(&crate::VERSION) > get_version_number("1.1.9"); + #[cfg(not(windows))] + return false; +} + +#[derive(Default)] +struct StateDisplay { + synced_displays: Vec, +} + +impl super::service::Reset for StateDisplay { + fn reset(&mut self) { + self.synced_displays.clear(); + } +} + +pub fn new() -> GenericService { + let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); + GenericService::repeat::(&svc.clone(), 300, run); + svc.sp +} + +fn check_get_displays_changed_msg(last_synced_displays: &mut Vec) -> Option { + let displays = try_get_displays().ok()?; + if displays.len() == last_synced_displays.len() { + return None; + } + + // Display to DisplayInfo + let displays = to_display_info(&displays); + if last_synced_displays.len() == 0 { + *last_synced_displays = displays; + None + } else { + let mut pi = PeerInfo { + ..Default::default() + }; + pi.displays = displays.clone(); + pi.current_display = 0; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + *last_synced_displays = displays; + Some(msg_out) + } +} + +#[cfg(all(windows, feature = "virtual_display_driver"))] +pub fn try_plug_out_virtual_display() { + let _res = virtual_display_manager::plug_out_headless(); +} + +fn run(sp: EmptyExtraFieldService, state: &mut StateDisplay) -> ResultType<()> { + if let Some(msg_out) = check_get_displays_changed_msg(&mut state.synced_displays) { + sp.send(msg_out); + log::info!("Displays changed"); + } + Ok(()) +} + +#[inline] +pub(super) fn get_original_resolution( + display_name: &str, + w: usize, + h: usize, +) -> MessageField { + #[cfg(all(windows, feature = "virtual_display_driver"))] + let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name); + #[cfg(not(all(windows, feature = "virtual_display_driver")))] + let is_virtual_display = false; + Some(if is_virtual_display { + Resolution { + width: 0, + height: 0, + ..Default::default() + } + } else { + let mut changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap(); + let (width, height) = match changed_resolutions.get(display_name) { + Some(res) => { + if res.changed.0 != w as i32 || res.changed.1 != h as i32 { + // If the resolution is changed by third process, remove the record in changed_resolutions. + changed_resolutions.remove(display_name); + (w as _, h as _) + } else { + res.original + } + } + None => (w as _, h as _), + }; + Resolution { + width, + height, + ..Default::default() + } + }) + .into() +} + +pub fn to_display_info(all: &Vec) -> Vec { + all.iter() + .map(|d| { + let display_name = d.name(); + let original_resolution = get_original_resolution(&display_name, d.width(), d.height()); + DisplayInfo { + x: d.origin().0 as _, + y: d.origin().1 as _, + width: d.width() as _, + height: d.height() as _, + name: display_name, + online: d.is_online(), + cursor_embedded: false, + original_resolution, + ..Default::default() + } + }) + .collect::>() +} + +pub fn is_inited_msg() -> Option { + #[cfg(target_os = "linux")] + if !scrap::is_x11() { + return super::wayland::is_inited(); + } + None +} + +pub async fn get_displays() -> ResultType> { + #[cfg(target_os = "linux")] + { + if !scrap::is_x11() { + return super::wayland::get_displays().await; + } + } + Ok(to_display_info(&try_get_displays()?)) +} + +#[inline] +pub fn get_primary() -> usize { + #[cfg(target_os = "linux")] + { + if !scrap::is_x11() { + return match super::wayland::get_primary() { + Ok(n) => n, + Err(_) => 0, + }; + } + } + + try_get_displays().map(|d| get_primary_2(&d)).unwrap_or(0) +} + +#[inline] +pub fn get_primary_2(all: &Vec) -> usize { + all.iter().position(|d| d.is_primary()).unwrap_or(0) +} + +#[inline] +#[cfg(all(windows, feature = "virtual_display_driver"))] +fn no_displays(displays: &Vec) -> bool { + let display_len = displays.len(); + if display_len == 0 { + true + } else if display_len == 1 { + let display = &displays[0]; + let dummy_display_side_max_size = 800; + display.width() <= dummy_display_side_max_size + && display.height() <= dummy_display_side_max_size + } else { + false + } +} + +#[inline] +#[cfg(not(all(windows, feature = "virtual_display_driver")))] +pub fn try_get_displays() -> ResultType> { + Ok(Display::all()?) +} + +#[cfg(all(windows, feature = "virtual_display_driver"))] +pub fn try_get_displays() -> ResultType> { + let mut displays = Display::all()?; + if no_displays(&displays) { + log::debug!("no displays, create virtual display"); + if let Err(e) = virtual_display_manager::plug_in_headless() { + log::error!("plug in headless failed {}", e); + } else { + displays = Display::all()?; + } + } + Ok(displays) +} diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b721149e9..d40bf02c1 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -17,7 +17,7 @@ use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; use std::{ convert::TryFrom, - ops::Sub, + ops::{Deref, DerefMut, Sub}, sync::atomic::{AtomicBool, Ordering}, thread, time::{self, Duration, Instant}, @@ -236,18 +236,43 @@ fn should_disable_numlock(evt: &KeyEvent) -> bool { pub const NAME_CURSOR: &'static str = "mouse_cursor"; pub const NAME_POS: &'static str = "mouse_pos"; -pub type MouseCursorService = ServiceTmpl; +#[derive(Clone)] +pub struct MouseCursorService { + pub sp: ServiceTmpl, +} -pub fn new_cursor() -> MouseCursorService { - let sp = MouseCursorService::new(NAME_CURSOR, true); - sp.repeat::(33, run_cursor); - sp +impl Deref for MouseCursorService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for MouseCursorService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +impl MouseCursorService { + pub fn new(name: String, need_snapshot: bool) -> Self { + Self { + sp: ServiceTmpl::::new(name, need_snapshot), + } + } +} + +pub fn new_cursor() -> ServiceTmpl { + let svc = MouseCursorService::new(NAME_CURSOR.to_owned(), true); + ServiceTmpl::::repeat::(&svc.clone(), 33, run_cursor); + svc.sp } pub fn new_pos() -> GenericService { - let sp = GenericService::new(NAME_POS, false); - sp.repeat::(33, run_pos); - sp + let svc = EmptyExtraFieldService::new(NAME_POS.to_owned(), false); + GenericService::repeat::(&svc.clone(), 33, run_pos); + svc.sp } #[inline] @@ -258,7 +283,7 @@ fn update_last_cursor_pos(x: i32, y: i32) { } } -fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { +fn run_pos(sp: EmptyExtraFieldService, state: &mut StatePos) -> ResultType<()> { let (_, (x, y)) = *LATEST_SYS_CURSOR_POS.lock().unwrap(); if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS { return Ok(()); @@ -988,7 +1013,6 @@ pub async fn lock_screen() { crate::platform::lock_screen(); } } - super::video_service::switch_to_primary().await; } pub fn handle_key(evt: &KeyEvent) { diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 155f2acb4..7b77c1516 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -25,7 +25,6 @@ use winapi::{ use crate::{ ipc::{self, new_listener, Connection, Data, DataPortableService}, platform::set_path_permission, - video_service::get_current_display, }; use super::video_qos; @@ -224,6 +223,8 @@ mod utils { pub mod server { use hbb_common::message_proto::PointerDeviceEvent; + use crate::display_service; + use super::*; lazy_static::lazy_static! { @@ -324,12 +325,17 @@ pub mod server { continue; } if c.is_none() { - *crate::video_service::CURRENT_DISPLAY.lock().unwrap() = current_display; - let Ok((_, _current, display)) = get_current_display() else { - log::error!("Failed to get current display"); + let Ok(mut displays) = display_service::try_get_displays() else { + log::error!("Failed to get displays"); *EXIT.lock().unwrap() = true; return; }; + if displays.len() <= current_display { + log::error!("Invalid display index:{}", current_display); + *EXIT.lock().unwrap() = true; + return; + } + let display = displays.remove(current_display); display_width = display.width(); display_height = display.height(); match Capturer::new(display, use_yuv) { @@ -522,6 +528,8 @@ pub mod server { pub mod client { use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent}; + use crate::display_service; + use super::*; lazy_static::lazy_static! { @@ -665,8 +673,8 @@ pub mod client { shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE)); } let (mut width, mut height) = (0, 0); - if let Ok((_, current, display)) = get_current_display() { - if current_display == current { + if let Ok(displays) = display_service::try_get_displays() { + if let Some(display) = displays.get(current_display) { width = display.width(); height = display.height(); } @@ -910,7 +918,15 @@ pub mod client { } if portable_service_running { log::info!("Create shared memory capturer"); - return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); + if current_display == *display_service::PRIMARY_DISPLAY_IDX { + return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); + } else { + bail!( + "Ignore capture display index: {}, the primary display index is: {}", + current_display, + *display_service::PRIMARY_DISPLAY_IDX + ); + } } else { log::debug!("Create capturer dxgi|gdi"); return Ok(Box::new( diff --git a/src/server/service.rs b/src/server/service.rs index 1cbed5acc..78a637776 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -1,16 +1,19 @@ use super::*; use std::{ collections::HashSet, + ops::{Deref, DerefMut}, thread::{self, JoinHandle}, time, }; pub trait Service: Send + Sync { - fn name(&self) -> &'static str; + fn name(&self) -> String; fn on_subscribe(&self, sub: ConnInner); fn on_unsubscribe(&self, id: i32); fn is_subed(&self, id: i32) -> bool; fn join(&self); + fn get_option(&self, opt: &str) -> Option; + fn set_option(&self, opt: &str, val: &str) -> Option; } pub trait Subscriber: Default + Send + Sync + 'static { @@ -20,12 +23,13 @@ pub trait Subscriber: Default + Send + Sync + 'static { #[derive(Default)] pub struct ServiceInner> { - name: &'static str, + name: String, handle: Option>, subscribes: HashMap, new_subscribes: HashMap, active: bool, need_snapshot: bool, + options: HashMap, } pub trait Reset { @@ -37,6 +41,35 @@ pub struct ServiceSwap>(ServiceTmpl); pub type GenericService = ServiceTmpl; pub const HIBERNATE_TIMEOUT: u64 = 30; pub const MAX_ERROR_TIMEOUT: u64 = 1_000; +pub const SERVICE_OPTION_VALUE_TRUE: &str = "1"; +pub const SERVICE_OPTION_VALUE_FALSE: &str = "0"; + +#[derive(Clone)] +pub struct EmptyExtraFieldService { + pub sp: GenericService, +} + +impl Deref for EmptyExtraFieldService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for EmptyExtraFieldService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +impl EmptyExtraFieldService { + pub fn new(name: String, need_snapshot: bool) -> Self { + Self { + sp: GenericService::new(name, need_snapshot), + } + } +} impl> ServiceInner { fn send_new_subscribes(&mut self, msg: Arc) { @@ -60,8 +93,8 @@ impl> ServiceInner { impl> Service for ServiceTmpl { #[inline] - fn name(&self) -> &'static str { - self.0.read().unwrap().name + fn name(&self) -> String { + self.0.read().unwrap().name.clone() } fn is_subed(&self, id: i32) -> bool { @@ -96,6 +129,18 @@ impl> Service for ServiceTmpl { } } } + + fn get_option(&self, opt: &str) -> Option { + self.0.read().unwrap().options.get(opt).cloned() + } + + fn set_option(&self, opt: &str, val: &str) -> Option { + self.0 + .write() + .unwrap() + .options + .insert(opt.to_string(), val.to_string()) + } } impl> Clone for ServiceTmpl { @@ -105,7 +150,7 @@ impl> Clone for ServiceTmpl { } impl> ServiceTmpl { - pub fn new(name: &'static str, need_snapshot: bool) -> Self { + pub fn new(name: String, need_snapshot: bool) -> Self { Self(Arc::new(RwLock::new(ServiceInner:: { name, active: true, @@ -114,6 +159,21 @@ impl> ServiceTmpl { }))) } + #[inline] + pub fn is_option_true(&self, opt: &str) -> bool { + self.get_option(opt) + .map_or(false, |v| v == SERVICE_OPTION_VALUE_TRUE) + } + + #[inline] + pub fn set_option_bool(&self, opt: &str, val: bool) { + if val { + self.set_option(opt, SERVICE_OPTION_VALUE_TRUE); + } else { + self.set_option(opt, SERVICE_OPTION_VALUE_FALSE); + } + } + #[inline] pub fn has_subscribes(&self) -> bool { self.0.read().unwrap().has_subscribes() @@ -189,14 +249,15 @@ impl> ServiceTmpl { } } - pub fn repeat(&self, interval_ms: u64, callback: F) + pub fn repeat(svc: &Svc, interval_ms: u64, callback: F) where - F: 'static + FnMut(Self, &mut S) -> ResultType<()> + Send, + F: 'static + FnMut(Svc, &mut S) -> ResultType<()> + Send, S: 'static + Default + Reset, + Svc: 'static + Clone + Send + DerefMut>, { let interval = time::Duration::from_millis(interval_ms); let mut callback = callback; - let sp = self.clone(); + let sp = svc.clone(); let thread = thread::spawn(move || { let mut state = S::default(); let mut may_reset = false; @@ -223,14 +284,15 @@ impl> ServiceTmpl { } log::info!("Service {} exit", sp.name()); }); - self.0.write().unwrap().handle = Some(thread); + svc.0.write().unwrap().handle = Some(thread); } - pub fn run(&self, callback: F) + pub fn run(svc: &Svc, callback: F) where - F: 'static + FnMut(Self) -> ResultType<()> + Send, + F: 'static + FnMut(Svc) -> ResultType<()> + Send, + Svc: 'static + Clone + Send + DerefMut>, { - let sp = self.clone(); + let sp = svc.clone(); let mut callback = callback; let thread = thread::spawn(move || { let mut error_timeout = HIBERNATE_TIMEOUT; @@ -259,7 +321,7 @@ impl> ServiceTmpl { } log::info!("Service {} exit", sp.name()); }); - self.0.write().unwrap().handle = Some(thread); + svc.0.write().unwrap().handle = Some(thread); } #[inline] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 07db5e3be..cfeeb1fa8 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -18,15 +18,11 @@ // to-do: // https://slhck.info/video/2017/03/01/rate-control.html -use super::{video_qos::VideoQoS, *}; -#[cfg(all(windows, feature = "virtual_display_driver"))] -use crate::virtual_display_manager; +use super::{service::ServiceTmpl, video_qos::VideoQoS, *}; #[cfg(windows)] use crate::{platform::windows::is_process_consent_running, privacy_win_mag}; -#[cfg(windows)] -use hbb_common::get_version_number; use hbb_common::{ - protobuf::MessageField, + anyhow::anyhow, tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, @@ -51,71 +47,18 @@ use std::{ }; pub const NAME: &'static str = "video"; - -struct ChangedResolution { - original: (i32, i32), - changed: (i32, i32), -} +pub const OPTION_DISPLAY_CHANGED: &'static str = "changed"; +pub const OPTION_REFRESH: &'static str = "refresh"; lazy_static::lazy_static! { - pub static ref CURRENT_DISPLAY: Arc> = Arc::new(Mutex::new(usize::MAX)); - static ref LAST_ACTIVE: Arc> = Arc::new(Mutex::new(Instant::now())); - static ref SWITCH: Arc> = Default::default(); static ref FRAME_FETCHED_NOTIFIER: (UnboundedSender<(i32, Option)>, Arc)>>>) = { let (tx, rx) = unbounded_channel(); (tx, Arc::new(TokioMutex::new(rx))) }; static ref PRIVACY_MODE_CONN_ID: Mutex = Mutex::new(0); - static ref IS_CAPTURER_MAGNIFIER_SUPPORTED: bool = is_capturer_mag_supported(); pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); - pub static ref LAST_SYNC_DISPLAYS: Arc>> = Default::default(); - static ref CHANGED_RESOLUTIONS: Arc>> = Default::default(); -} - -#[inline] -pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) { - let mut lock = CHANGED_RESOLUTIONS.write().unwrap(); - match lock.get_mut(display_name) { - Some(res) => res.changed = changed, - None => { - lock.insert( - display_name.to_owned(), - ChangedResolution { original, changed }, - ); - } - } -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn reset_resolutions() { - for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() { - let (w, h) = res.original; - if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) { - log::error!( - "Failed to reset resolution of display '{}' to ({},{}): {}", - name, - w, - h, - e - ); - } - } -} - -#[inline] -fn is_capturer_mag_supported() -> bool { - #[cfg(windows)] - return scrap::CapturerMag::is_supported(); - #[cfg(not(windows))] - false -} - -#[inline] -pub fn capture_cursor_embedded() -> bool { - scrap::is_cursor_embedded() } #[inline] @@ -133,15 +76,6 @@ pub fn get_privacy_mode_conn_id() -> i32 { *PRIVACY_MODE_CONN_ID.lock().unwrap() } -#[inline] -pub fn is_privacy_mode_supported() -> bool { - #[cfg(windows)] - return *IS_CAPTURER_MAGNIFIER_SUPPORTED - && get_version_number(&crate::VERSION) > get_version_number("1.1.9"); - #[cfg(not(windows))] - return false; -} - struct VideoFrameController { cur: Instant, send_conn_ids: HashSet, @@ -192,10 +126,37 @@ impl VideoFrameController { } } -pub fn new() -> GenericService { - let sp = GenericService::new(NAME, true); - sp.run(run); - sp +#[derive(Clone)] +pub struct VideoService { + sp: GenericService, + idx: usize, +} + +impl Deref for VideoService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for VideoService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +pub fn get_service_name(idx: usize) -> String { + format!("{}{}", NAME, idx) +} + +pub fn new(idx: usize) -> GenericService { + let vs = VideoService { + sp: GenericService::new(get_service_name(idx), true), + idx, + }; + GenericService::run(&vs, run); + vs.sp } fn check_display_changed( @@ -221,19 +182,10 @@ fn check_display_changed( if n != last_n { return true; }; - - for (i, d) in displays.iter().enumerate() { - if d.is_primary() { - if i != last_current { - return true; - }; - if d.width() != last_width || d.height() != last_height { - return true; - }; - } + match displays.get(last_current) { + Some(d) => d.width() != last_width || d.height() != last_height, + None => true, } - - return false; } // Capturer object is expensive, avoiding to create it frequently. @@ -317,14 +269,27 @@ fn create_capturer( } // This function works on privacy mode. Windows only for now. -pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> String { +pub fn test_create_capturer( + privacy_mode_id: i32, + display_idx: usize, + timeout_millis: u64, +) -> String { let test_begin = Instant::now(); loop { - let err = match get_current_display() { - Ok((_, current, display)) => { - match create_capturer(privacy_mode_id, display, true, current, false) { - Ok(_) => return "".to_owned(), - Err(e) => e, + let err = match try_get_displays() { + Ok(mut displays) => { + if displays.len() <= display_idx { + anyhow!( + "Failed to get display {}, the displays' count is {}", + display_idx, + displays.len() + ) + } else { + let display = displays.remove(display_idx); + match create_capturer(privacy_mode_id, display, true, display_idx, false) { + Ok(_) => return "".to_owned(), + Err(e) => e, + } } } Err(e) => e, @@ -352,6 +317,7 @@ fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> Resu } pub(super) struct CapturerInfo { + pub name: String, pub origin: (i32, i32), pub width: usize, pub height: usize, @@ -376,7 +342,11 @@ impl DerefMut for CapturerInfo { } } -fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType { +fn get_capturer( + current: usize, + use_yuv: bool, + portable_service_running: bool, +) -> ResultType { #[cfg(target_os = "linux")] { if !scrap::is_x11() { @@ -384,8 +354,18 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType ResultType Option> { - let displays = try_get_displays().ok()?; - let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap(); - if displays.len() != last_sync_displays.len() { - // No need to check if the resolutions are changed by third process. - Some(displays) - } else { - None - } -} - -fn check_get_displays_changed_msg() -> Option { - let displays = check_displays_new()?; - // Display to DisplayInfo - let (current, displays) = get_displays_2(&displays); - let mut pi = PeerInfo { - ..Default::default() - }; - pi.displays = displays.clone(); - pi.current_display = current as _; - let mut msg_out = Message::new(); - msg_out.set_peer_info(pi); - *LAST_SYNC_DISPLAYS.write().unwrap() = displays; - Some(msg_out) -} - -#[cfg(all(windows, feature = "virtual_display_driver"))] -pub fn try_plug_out_virtual_display() { - let _res = virtual_display_manager::plug_out_headless(); -} - -fn run(sp: GenericService) -> ResultType<()> { +fn run(vs: VideoService) -> ResultType<()> { #[cfg(not(any(target_os = "android", target_os = "ios")))] let _wake_lock = get_wake_lock(); // ensure_inited() is needed because clear() may be called. + // to-do: wayland ensure_inited should pass current display index. + // But for now, we do not support multi-screen capture on wayland. #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; #[cfg(windows)] @@ -483,7 +435,9 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(not(windows))] let last_portable_service_running = false; - let mut c = get_capturer(true, last_portable_service_running)?; + let display_idx = vs.idx; + let sp = vs.sp; + let mut c = get_capturer(display_idx, true, last_portable_service_running)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); video_qos.refresh(None); @@ -506,37 +460,18 @@ fn run(sp: GenericService) -> ResultType<()> { c.set_use_yuv(encoder.use_yuv()); VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); - if *SWITCH.lock().unwrap() { - log::debug!("Broadcasting display switch"); - let mut misc = Misc::new(); - let display_name = get_current_display() - .map(|(_, _, d)| d.name()) - .unwrap_or_default(); - let original_resolution = get_original_resolution(&display_name, c.width, c.height); - misc.set_switch_display(SwitchDisplay { - display: c.current as _, - x: c.origin.0 as _, - y: c.origin.1 as _, - width: c.width as _, - height: c.height as _, - cursor_embedded: capture_cursor_embedded(), - #[cfg(not(any(target_os = "android", target_os = "ios")))] - resolutions: Some(SupportedResolutions { - resolutions: if display_name.is_empty() { - vec![] - } else { - crate::platform::resolutions(&display_name) - }, - ..SupportedResolutions::default() - }) - .into(), - original_resolution, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - *SWITCH.lock().unwrap() = false; - sp.send(msg_out); + if sp.is_option_true(OPTION_DISPLAY_CHANGED) { + log::debug!("Broadcasting display changed"); + broadcast_display_changed( + display_idx, + &sp, + Some((c.name.clone(), c.origin.clone(), c.width, c.height)), + ); + sp.set_option_bool(OPTION_DISPLAY_CHANGED, false); + } + + if sp.is_option_true(OPTION_REFRESH) { + sp.set_option_bool(OPTION_REFRESH, false); } let mut frame_controller = VideoFrameController::new(); @@ -572,13 +507,7 @@ fn run(sp: GenericService) -> ResultType<()> { } drop(video_qos); - if *SWITCH.lock().unwrap() { - bail!("SWITCH"); - } - if c.current != *CURRENT_DISPLAY.lock().unwrap() { - #[cfg(target_os = "linux")] - super::wayland::clear(); - *SWITCH.lock().unwrap() = true; + if sp.is_option_true(OPTION_DISPLAY_CHANGED) || sp.is_option_true(OPTION_REFRESH) { bail!("SWITCH"); } if codec_name != Encoder::negotiated_codec() { @@ -604,23 +533,12 @@ fn run(sp: GenericService) -> ResultType<()> { // Capturer on macos does not return Err event the solution is changed. #[cfg(target_os = "macos")] if check_display_changed(c.ndisplay, c.current, c.width, c.height) { + sp.set_option_bool(OPTION_DISPLAY_CHANGED, true); log::info!("Displays changed"); - *SWITCH.lock().unwrap() = true; - bail!("SWITCH"); - } - - if let Some(msg_out) = check_get_displays_changed_msg() { - sp.send(msg_out); - log::info!("Displays changed"); - #[cfg(target_os = "linux")] - super::wayland::clear(); - *SWITCH.lock().unwrap() = true; bail!("SWITCH"); } } - *LAST_ACTIVE.lock().unwrap() = now; - frame_controller.reset(); #[cfg(any(target_os = "android", target_os = "ios"))] @@ -631,8 +549,14 @@ fn run(sp: GenericService) -> ResultType<()> { match frame { scrap::Frame::RAW(data) => { if data.len() != 0 { - let send_conn_ids = - handle_one_frame(&sp, data, ms, &mut encoder, recorder.clone())?; + let send_conn_ids = handle_one_frame( + display_idx, + &sp, + data, + ms, + &mut encoder, + recorder.clone(), + )?; frame_controller.set_send(now, send_conn_ids); } } @@ -649,7 +573,7 @@ fn run(sp: GenericService) -> ResultType<()> { let time = now - start; let ms = (time.as_secs() * 1000 + time.subsec_millis() as u64) as i64; let send_conn_ids = - handle_one_frame(&sp, &frame, ms, &mut encoder, recorder.clone())?; + handle_one_frame(display_idx, &sp, &frame, ms, &mut encoder, recorder.clone())?; frame_controller.set_send(now, send_conn_ids); #[cfg(windows)] { @@ -693,7 +617,7 @@ fn run(sp: GenericService) -> ResultType<()> { log::info!("Displays changed"); #[cfg(target_os = "linux")] super::wayland::clear(); - *SWITCH.lock().unwrap() = true; + sp.set_option_bool(OPTION_DISPLAY_CHANGED, true); bail!("SWITCH"); } @@ -829,6 +753,7 @@ fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> Resu #[inline] fn handle_one_frame( + display: usize, sp: &GenericService, frame: &[u8], ms: i64, @@ -844,7 +769,10 @@ fn handle_one_frame( })?; let mut send_conn_ids: HashSet = Default::default(); - if let Ok(msg) = encoder.encode_to_message(frame, ms) { + if let Ok(mut vf) = encoder.encode_to_message(frame, ms) { + vf.display = display as _; + let mut msg = Message::new(); + msg.set_video_frame(vf); #[cfg(not(target_os = "ios"))] recorder .lock() @@ -856,69 +784,6 @@ fn handle_one_frame( Ok(send_conn_ids) } -#[inline] -fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageField { - #[cfg(all(windows, feature = "virtual_display_driver"))] - let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name); - #[cfg(not(all(windows, feature = "virtual_display_driver")))] - let is_virtual_display = false; - Some(if is_virtual_display { - Resolution { - width: 0, - height: 0, - ..Default::default() - } - } else { - let mut changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap(); - let (width, height) = match changed_resolutions.get(display_name) { - Some(res) => { - if res.changed.0 != w as i32 || res.changed.1 != h as i32 { - // If the resolution is changed by third process, remove the record in changed_resolutions. - changed_resolutions.remove(display_name); - (w as _, h as _) - } else { - res.original - } - } - None => (w as _, h as _), - }; - Resolution { - width, - height, - ..Default::default() - } - }) - .into() -} - -pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { - let mut displays = Vec::new(); - let mut primary = 0; - for (i, d) in all.iter().enumerate() { - if d.is_primary() { - primary = i; - } - let display_name = d.name(); - let original_resolution = get_original_resolution(&display_name, d.width(), d.height()); - displays.push(DisplayInfo { - x: d.origin().0 as _, - y: d.origin().1 as _, - width: d.width() as _, - height: d.height() as _, - name: display_name, - online: d.is_online(), - cursor_embedded: false, - original_resolution, - ..Default::default() - }); - } - let mut lock = CURRENT_DISPLAY.lock().unwrap(); - if *lock >= displays.len() { - *lock = primary - } - (*lock, displays) -} - pub fn is_inited_msg() -> Option { #[cfg(target_os = "linux")] if !scrap::is_x11() { @@ -927,65 +792,10 @@ pub fn is_inited_msg() -> Option { None } -// switch to primary display if long time (30 seconds) no users -#[inline] -pub fn try_reset_current_display() { - if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 { - *CURRENT_DISPLAY.lock().unwrap() = usize::MAX; - } - *LAST_ACTIVE.lock().unwrap() = time::Instant::now(); -} - -pub async fn get_displays() -> ResultType<(usize, Vec)> { - #[cfg(target_os = "linux")] - { - if !scrap::is_x11() { - return super::wayland::get_displays().await; - } - } - Ok(get_displays_2(&try_get_displays()?)) -} - -pub async fn switch_display(i: i32) { - let i = i as usize; - if let Ok((_, displays)) = get_displays().await { - if i < displays.len() { - *CURRENT_DISPLAY.lock().unwrap() = i; - } - } -} - #[inline] pub fn refresh() { #[cfg(target_os = "android")] Display::refresh_size(); - *SWITCH.lock().unwrap() = true; -} - -fn get_primary() -> usize { - #[cfg(target_os = "linux")] - { - if !scrap::is_x11() { - return match super::wayland::get_primary() { - Ok(n) => n, - Err(_) => 0, - }; - } - } - - if let Ok(all) = try_get_displays() { - for (i, d) in all.iter().enumerate() { - if d.is_primary() { - return i; - } - } - } - 0 -} - -#[inline] -pub async fn switch_to_primary() { - switch_display(get_primary() as _).await; } #[inline] @@ -1034,30 +844,6 @@ fn try_get_displays() -> ResultType> { Ok(Display::all()?) } -pub(super) fn get_current_display_2(mut all: Vec) -> ResultType<(usize, usize, Display)> { - let mut current = *CURRENT_DISPLAY.lock().unwrap() as usize; - if all.len() == 0 { - bail!("No displays"); - } - let n = all.len(); - if current >= n { - current = 0; - for (i, d) in all.iter().enumerate() { - if d.is_primary() { - current = i; - break; - } - } - *CURRENT_DISPLAY.lock().unwrap() = current; - } - return Ok((n, current, all.remove(current))); -} - -#[inline] -pub fn get_current_display() -> ResultType<(usize, usize, Display)> { - get_current_display_2(try_get_displays()?) -} - #[cfg(windows)] fn start_uac_elevation_check() { static START: Once = Once::new(); @@ -1090,3 +876,63 @@ fn get_wake_lock() -> crate::platform::WakeLock { }; crate::platform::WakeLock::new(display, idle, sleep) } + +#[inline] +fn broadcast_display_changed( + display_idx: usize, + sp: &GenericService, + display_meta: Option<(String, (i32, i32), usize, usize)>, +) { + if let Some(msg_out) = make_display_changed_msg(display_idx, display_meta) { + sp.send(msg_out); + } +} + +fn get_display_info_simple_meta(display_idx: usize) -> Option<(String, (i32, i32), usize, usize)> { + let displays = display_service::try_get_displays().ok()?; + if let Some(display) = displays.get(display_idx) { + Some(( + display.name(), + display.origin(), + display.width(), + display.height(), + )) + } else { + None + } +} + +pub fn make_display_changed_msg( + display_idx: usize, + display_meta: Option<(String, (i32, i32), usize, usize)>, +) -> Option { + let mut misc = Misc::new(); + let (name, origin, width, height) = match display_meta { + Some(d) => d, + None => get_display_info_simple_meta(display_idx)?, + }; + let original_resolution = display_service::get_original_resolution(&name, width, height); + misc.set_switch_display(SwitchDisplay { + display: display_idx as _, + x: origin.0, + y: origin.1, + width: width as _, + height: height as _, + cursor_embedded: display_service::capture_cursor_embedded(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + resolutions: Some(SupportedResolutions { + resolutions: if name.is_empty() { + vec![] + } else { + crate::platform::resolutions(&name) + }, + ..SupportedResolutions::default() + }) + .into(), + original_resolution, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + Some(msg_out) +} diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 44cf8bf4a..f26b27b20 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -142,9 +142,11 @@ pub(super) async fn check_init() -> ResultType<()> { if *CAP_DISPLAY_INFO.read().unwrap() == 0 { let mut lock = CAP_DISPLAY_INFO.write().unwrap(); if *lock == 0 { - let all = Display::all()?; + let mut all = Display::all()?; let num = all.len(); - let (primary, mut displays) = super::video_service::get_displays_2(&all); + let primary = super::display_service::get_primary_2(&all); + let current = primary; + let mut displays = super::display_service::to_display_info(&all); for display in displays.iter_mut() { display.cursor_embedded = is_cursor_embedded(); } @@ -154,12 +156,11 @@ pub(super) async fn check_init() -> ResultType<()> { rects.push((d.origin(), d.width(), d.height())); } - let (ndisplay, current, display) = - super::video_service::get_current_display_2(all)?; + let display = all.remove(current); let (origin, width, height) = (display.origin(), display.width(), display.height()); log::debug!( "#displays={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}", - ndisplay, + num, current, &origin, width, @@ -213,16 +214,14 @@ pub(super) async fn check_init() -> ResultType<()> { Ok(()) } -pub(super) async fn get_displays() -> ResultType<(usize, Vec)> { +pub(super) async fn get_displays() -> ResultType> { check_init().await?; let addr = *CAP_DISPLAY_INFO.read().unwrap(); if addr != 0 { let cap_display_info: *const CapDisplayInfo = addr as _; unsafe { let cap_display_info = &*cap_display_info; - let primary = cap_display_info.primary; - let displays = cap_display_info.displays.clone(); - Ok((primary, displays)) + Ok(cap_display_info.displays.clone()) } } else { bail!("Failed to get capturer display info"); @@ -268,6 +267,9 @@ pub(super) fn get_capturer() -> ResultType { let cap_display_info = &*cap_display_info; let rect = cap_display_info.rects[cap_display_info.current]; Ok(super::video_service::CapturerInfo { + name: cap_display_info.displays[cap_display_info.current] + .name + .clone(), origin: rect.0, width: rect.1, height: rect.2, diff --git a/src/ui.rs b/src/ui.rs index 27793b31f..c8f044421 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,7 +1,6 @@ use std::{ collections::HashMap, iter::FromIterator, - process::Child, sync::{Arc, Mutex}, }; @@ -22,7 +21,6 @@ mod cm; pub mod inline; pub mod remote; -pub type Children = Arc)>>; #[allow(dead_code)] type Status = (i32, bool, i64, String); @@ -34,7 +32,6 @@ lazy_static::lazy_static! { #[cfg(not(any(feature = "flutter", feature = "cli")))] lazy_static::lazy_static! { pub static ref CUR_SESSION: Arc>>> = Default::default(); - static ref CHILDREN : Children = Default::default(); } struct UIHostHandler; diff --git a/src/ui/header.tis b/src/ui/header.tis index 2adc37027..f4a05b9f7 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -301,7 +301,7 @@ class Header: Reactor.Component { if (recording) handler.refresh_video(); else - handler.record_screen(false, display_width, display_height); + handler.record_screen(false, 0, display_width, display_height); } event click $(#screen) (_, me) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 95ea977e2..979d6d7f6 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -122,7 +122,11 @@ impl InvokeUiSession for SciterHandler { "updateQualityStatus", &make_args!( status.speed.map_or(Value::null(), |it| it.into()), - status.fps.map_or(Value::null(), |it| it.into()), + status + .fps + .iter() + .next() + .map_or(Value::null(), |(_, v)| (*v).into()), status.delay.map_or(Value::null(), |it| it.into()), status.target_bitrate.map_or(Value::null(), |it| it.into()), status @@ -223,7 +227,7 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, rgba: &mut scrap::ImageRgb) { + fn on_rgba(&self, _display: usize, rgba: &mut scrap::ImageRgb) { VIDEO .lock() .unwrap() @@ -304,11 +308,11 @@ impl InvokeUiSession for SciterHandler { } /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. - fn get_rgba(&self) -> *const u8 { + fn get_rgba(&self, _display: usize) -> *const u8 { std::ptr::null() } - fn next_rgba(&self) {} + fn next_rgba(&self, _display: usize) {} } pub struct SciterSession(Session); @@ -450,7 +454,7 @@ impl sciter::EventHandler for SciterSession { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(); - fn record_screen(bool, i32, i32); + fn record_screen(bool, i32, i32, i32); fn record_status(bool); fn get_toggle_option(String); fn is_privacy_mode_supported(); diff --git a/src/ui/remote.tis b/src/ui/remote.tis index e9694ccc2..022d43668 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -23,7 +23,7 @@ handler.setDisplay = function(x, y, w, h, cursor_embedded) { display_origin_y = y; display_cursor_embedded = cursor_embedded; adaptDisplay(); - if (recording) handler.record_screen(true, w, h); + if (recording) handler.record_screen(true, 0, w, h); } // in case toolbar not shown correctly @@ -478,7 +478,7 @@ function self.closing() { var (x, y, w, h) = view.box(#rectw, #border, #screen); if (is_file_transfer) save_file_transfer_close_state(); if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); - if (recording) handler.record_screen(false, display_width, display_height); + if (recording) handler.record_screen(false, 0, display_width, display_height); } var qualityMonitor; diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index ceb185443..da36646e6 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -164,6 +164,7 @@ impl ConnectionManager { } #[inline] + #[cfg(windows)] fn is_authorized(&self, id: i32) -> bool { CLIENTS .read() diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ed2b4f4fc..b338e68c5 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -2,14 +2,14 @@ use hbb_common::password_security; use hbb_common::{ allow_err, - config::{self, Config, LocalConfig, PeerConfig}, - directories_next, log, tokio, -}; -use hbb_common::{ bytes::Bytes, + config::{self, Config, LocalConfig, PeerConfig}, config::{CONNECT_TIMEOUT, RENDEZVOUS_PORT}, + directories_next, futures::future::join_all, + log, rendezvous_proto::*, + tokio, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::{ @@ -17,9 +17,10 @@ use hbb_common::{ tokio::{sync::mpsc, time}, }; use serde_derive::Serialize; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::process::Child; use std::{ collections::HashMap, - process::Child, sync::{Arc, Mutex}, }; @@ -31,6 +32,7 @@ use crate::ipc; type Message = RendezvousMessage; +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub type Children = Arc)>>; #[derive(Clone, Debug, Serialize)] @@ -594,7 +596,13 @@ pub fn current_is_wayland() -> bool { #[inline] pub fn get_new_version() -> String { - (*SOFTWARE_UPDATE_URL.lock().unwrap().rsplit('/').next().unwrap_or("")).to_string() + (*SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .rsplit('/') + .next() + .unwrap_or("")) + .to_string() } #[inline] diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index d932fd112..0fccabed2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -7,10 +7,7 @@ use std::{collections::HashMap, sync::atomic::AtomicBool}; use std::{ ops::{Deref, DerefMut}, str::FromStr, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Mutex, RwLock, - }, + sync::{atomic::Ordering, Arc, Mutex, RwLock}, time::SystemTime, }; use uuid::Uuid; @@ -28,7 +25,7 @@ use hbb_common::{ sync::mpsc, time::{Duration as TokioDuration, Instant}, }, - SessionID, Stream, + Stream, }; use crate::client::io_loop::Remote; @@ -49,8 +46,7 @@ const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15; #[derive(Clone, Default)] pub struct Session { - pub session_id: SessionID, // different from the one in LoginConfigHandler, used for flutter UI message pass - pub id: String, // peer id + pub id: String, // peer id pub password: String, pub args: Vec, pub lc: Arc>, @@ -286,12 +282,22 @@ impl Session { && !self.lc.read().unwrap().disable_clipboard.v } - pub fn refresh_video(&self) { - self.send(Data::Message(LoginConfigHandler::refresh())); + pub fn refresh_video(&self, display: usize) { + if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) { + self.send(Data::Message(LoginConfigHandler::refresh_display(display))); + } else { + self.send(Data::Message(LoginConfigHandler::refresh())); + } } - pub fn record_screen(&self, start: bool, w: i32, h: i32) { - self.send(Data::RecordScreen(start, w, h, self.id.clone())); + pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) { + self.send(Data::RecordScreen( + start, + display as usize, + w, + h, + self.id.clone(), + )); } pub fn record_status(&self, status: bool) { @@ -603,6 +609,19 @@ impl Session { self.send(Data::Message(msg_out)); } + pub fn capture_displays(&self, add: Vec, sub: Vec, set: Vec) { + let mut misc = Misc::new(); + misc.set_capture_displays(CaptureDisplays { + add, + sub, + set, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + pub fn switch_display(&self, display: i32) { let (w, h) = match self.lc.read().unwrap().get_custom_resolution(display) { Some((w, h)) => (w, h), @@ -1164,7 +1183,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn update_block_input_state(&self, on: bool); fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); fn adapt_size(&self); - fn on_rgba(&self, rgba: &mut scrap::ImageRgb); + fn on_rgba(&self, display: usize, rgba: &mut scrap::ImageRgb); fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); #[cfg(any(target_os = "android", target_os = "ios"))] fn clipboard(&self, content: String); @@ -1175,8 +1194,8 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn on_voice_call_closed(&self, reason: &str); fn on_voice_call_waiting(&self); fn on_voice_call_incoming(&self); - fn get_rgba(&self) -> *const u8; - fn next_rgba(&self); + fn get_rgba(&self, display: usize) -> *const u8; + fn next_rgba(&self, display: usize); } impl Deref for Session { @@ -1237,7 +1256,7 @@ impl Interface for Session { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); - self.msgbox("error", "Remote Error", "No Display", ""); + self.msgbox("error", "Remote Error", "No Displays", ""); return; } self.try_change_init_resolution(pi.current_display); @@ -1447,24 +1466,29 @@ pub async fn io_loop(handler: Session, round: u32) { } return; } - let frame_count = Arc::new(AtomicUsize::new(0)); - let frame_count_cl = frame_count.clone(); + let frame_count_map: Arc>> = Default::default(); + let frame_count_map_cl = frame_count_map.clone(); let ui_handler = handler.ui_handler.clone(); - let (video_sender, audio_sender, video_queue, decode_fps) = - start_video_audio_threads(move |data: &mut scrap::ImageRgb| { - frame_count_cl.fetch_add(1, Ordering::Relaxed); - ui_handler.on_rgba(data); - }); + let (video_sender, audio_sender, video_queue_map, decode_fps_map) = start_video_audio_threads( + handler.clone(), + move |display: usize, data: &mut scrap::ImageRgb| { + let mut write_lock = frame_count_map_cl.write().unwrap(); + let count = write_lock.get(&display).unwrap_or(&0) + 1; + write_lock.insert(display, count); + drop(write_lock); + ui_handler.on_rgba(display, data); + }, + ); let mut remote = Remote::new( handler, - video_queue, + video_queue_map, video_sender, audio_sender, receiver, sender, - frame_count, - decode_fps, + frame_count_map, + decode_fps_map, ); remote.io_loop(&key, &token, round).await; remote.sync_jobs_status_to_local().await; From 2f2a7d1f89ccfb1684fff70fc6b7d962c56c6110 Mon Sep 17 00:00:00 2001 From: dignow Date: Sun, 8 Oct 2023 22:32:48 +0800 Subject: [PATCH 02/10] feat, multi flutter ui sessions, change settings to 'Display' Signed-off-by: dignow --- flutter/lib/common/widgets/peer_card.dart | 2 +- .../desktop/pages/desktop_setting_page.dart | 48 +++++++++---------- src/lang/ar.rs | 4 +- src/lang/ca.rs | 4 +- src/lang/cn.rs | 4 +- src/lang/cs.rs | 4 +- src/lang/da.rs | 4 +- src/lang/de.rs | 4 +- src/lang/el.rs | 4 +- src/lang/eo.rs | 4 +- src/lang/es.rs | 4 +- src/lang/fa.rs | 4 +- src/lang/fr.rs | 4 +- src/lang/hu.rs | 4 +- src/lang/id.rs | 4 +- src/lang/it.rs | 4 +- src/lang/ja.rs | 4 +- src/lang/ko.rs | 4 +- src/lang/kz.rs | 4 +- src/lang/lt.rs | 4 +- src/lang/lv.rs | 4 +- src/lang/nl.rs | 4 +- src/lang/pl.rs | 4 +- src/lang/pt_PT.rs | 4 +- src/lang/ptbr.rs | 4 +- src/lang/ro.rs | 4 +- src/lang/ru.rs | 4 +- src/lang/sk.rs | 4 +- src/lang/sl.rs | 4 +- src/lang/sq.rs | 4 +- src/lang/sr.rs | 4 +- src/lang/sv.rs | 4 +- src/lang/template.rs | 4 +- src/lang/th.rs | 4 +- src/lang/tr.rs | 4 +- src/lang/tw.rs | 4 +- src/lang/ua.rs | 4 +- src/lang/vn.rs | 4 +- 38 files changed, 97 insertions(+), 97 deletions(-) diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index a843c3d41..922f88225 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -600,7 +600,7 @@ abstract class BasePeerCard extends StatelessWidget { await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs); _openInWindowsAction(String id) async => await _openNewConnInAction( - id, 'Open in New Window', kOptionOpenInWindows); + id, 'Open in new window', kOptionOpenInWindows); _openNewConnInOptAction(String id) async => mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 905c082a0..9daaea9f6 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -269,7 +269,6 @@ class _GeneralState extends State<_General> { service(), theme(), hwcodec(), - chooseDisplay(), audio(context), record(context), _Card(title: 'Language', children: [language()]), @@ -378,29 +377,6 @@ class _GeneralState extends State<_General> { ); } - Widget chooseDisplay() { - if (!useTextureRender) return const Offstage(); - - var current = getChooseDisplayBehavior(); - onChanged(String value) { - bind.mainSetOption(key: kKeyChooseDisplayBehavior, value: value); - setState(() {}); - } - - return _Card(title: 'Choose Display Behavior', children: [ - _Radio(context, - value: kChooseDisplayBehaviorSwitch, - groupValue: current, - label: 'Switch Display', - onChanged: onChanged), - _Radio(context, - value: kChooseDisplayBehaviorOpen, - groupValue: current, - label: 'Open in New Window', - onChanged: onChanged), - ]); - } - Widget audio(BuildContext context) { String getDefault() { if (Platform.isWindows) return translate('System Sound'); @@ -1148,6 +1124,7 @@ class _DisplayState extends State<_Display> { controller: scrollController, physics: DraggableNeverScrollableScrollPhysics(), children: [ + chooseDisplay(context), viewStyle(context), scrollStyle(context), imageQuality(context), @@ -1156,6 +1133,29 @@ class _DisplayState extends State<_Display> { ]).marginOnly(bottom: _kListViewBottomMargin)); } + Widget chooseDisplay(BuildContext context) { + if (!useTextureRender) return const Offstage(); + + var current = getChooseDisplayBehavior(); + onChanged(String value) { + bind.mainSetOption(key: kKeyChooseDisplayBehavior, value: value); + setState(() {}); + } + + return _Card(title: 'Choose Display Behavior', children: [ + _Radio(context, + value: kChooseDisplayBehaviorSwitch, + groupValue: current, + label: 'Switch display', + onChanged: onChanged), + _Radio(context, + value: kChooseDisplayBehaviorOpen, + groupValue: current, + label: 'Open in new window', + onChanged: onChanged), + ]); + } + Widget viewStyle(BuildContext context) { final key = 'view_style'; onChanged(String value) async { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 30c8bea5f..99bb7c3e0 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index d9c570796..b4938f8e5 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 24d2ec78a..15a74e11c 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", "没有显示器。"), ("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"), ("Choose Display Behavior", "选择显示器的行为"), - ("Switch Display", "切换显示器"), - ("Open in New Window", "在新的窗口中打开"), + ("Switch display", "切换显示器"), + ("Open in new window", "在新的窗口中打开"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 4f54e0540..df57c1d9e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 17792e10b..6f9092230 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 0628846d3..55f7b2dd0 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index fa545e182..597829043 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 56f24354c..03692ff77 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 46b0dc63c..de1ec9fed 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 802b6473c..07829de99 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index bec161910..810dbddc1 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 9fb02464e..ce8cb5add 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ac6b1276d..f1f18ca5c 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 8d6f94b00..be1157776 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -564,7 +564,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7bad54921..43daac2de 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 5d0b7eab7..94ad160f8 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 24f97be4b..81003b1b2 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index b35f33498..d77e543ac 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index ffa33d061..d10238608 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index f67b3dd23..f5a1d5322 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 81c23ef84..4f0fe343c 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 360cfd4fa..e794b9996 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8df3e9ea8..a75da7507 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 1822d79f2..f03d0b96d 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index a0be18469..302a4e209 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 75bb447f4..be35fa666 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 5f9f8d5f3..311106d74 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 3fef0845d..31a2096bf 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 3382ac2b6..dc11a8af0 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index eca0c4f19..ffeaea2fc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 256bb7d76..a62790a5d 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 5477cb666..eb9dd789f 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index c8d323d8a..acf67740d 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index ad37066f3..a89a4b547 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7b510a224..5e10c9383 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 309d34f7a..860cd2dbd 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", ""), ("elevated_switch_display_msg", ""), ("Choose Display Behavior", ""), - ("Switch Display", ""), - ("Open in New Window", ""), + ("Switch display", ""), + ("Open in new window", ""), ].iter().cloned().collect(); } From 3bb7123dd50834bb8de7f36e36a04b495b409c16 Mon Sep 17 00:00:00 2001 From: dignow Date: Sun, 8 Oct 2023 23:32:11 +0800 Subject: [PATCH 03/10] fix build, sciter Signed-off-by: dignow --- src/client.rs | 2 +- src/client/io_loop.rs | 2 +- src/flutter_ffi.rs | 2 +- src/server/connection.rs | 2 ++ src/ui/header.tis | 6 ++++-- src/ui/remote.rs | 2 +- src/ui_session_interface.rs | 10 ++++++++-- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/client.rs b/src/client.rs index f7c6c6d96..eff490ebb 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1926,7 +1926,7 @@ where // // to-do: fix the error log::error!("handle video frame error, {}", e); - session.refresh_video(display); + session.refresh_video(display as _); } _ => {} } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 151bee0cc..4c830a2f9 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1008,7 +1008,7 @@ impl Remote { { // Refresh causes client set_display, left frames cause flickering. while let Some(_) = video_queue.pop() {} - self.handler.refresh_video(*display); + self.handler.refresh_video(*display as _); ctl.refresh_times += 1; ctl.last_refresh_instant = Instant::now(); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 080ad8178..77da2942c 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -180,7 +180,7 @@ pub fn session_close(session_id: SessionID) { pub fn session_refresh(session_id: SessionID, display: usize) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.refresh_video(display); + session.refresh_video(display as _); } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 990ee733c..5f9502397 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1970,6 +1970,8 @@ impl Connection { } Some(misc::Union::RefreshVideo(r)) => { if r { + // Refresh all videos. + // Compatibility with old versions and sciter(remote). self.refresh_video_display(None); } self.update_auto_disconnect_timer(); diff --git a/src/ui/header.tis b/src/ui/header.tis index f4a05b9f7..a6cb2645d 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -298,8 +298,9 @@ class Header: Reactor.Component { recording = !recording; header.update(); handler.record_status(recording); + // 0 is just a dummy value. It will be ignored by the handler. if (recording) - handler.refresh_video(); + handler.refresh_video(0); else handler.record_screen(false, 0, display_width, display_height); } @@ -370,7 +371,8 @@ class Header: Reactor.Component { } event click $(#refresh) { - handler.refresh_video(); + // 0 is just a dummy value. It will be ignored by the handler. + handler.refresh_video(0); } event click $(#block-input) { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 979d6d7f6..71ef8f84d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -453,7 +453,7 @@ impl sciter::EventHandler for SciterSession { fn save_view_style(String); fn save_image_quality(String); fn save_custom_image_quality(i32); - fn refresh_video(); + fn refresh_video(i32); fn record_screen(bool, i32, i32, i32); fn record_status(bool); fn get_toggle_option(String); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 0fccabed2..ff559f19c 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -282,14 +282,20 @@ impl Session { && !self.lc.read().unwrap().disable_clipboard.v } - pub fn refresh_video(&self, display: usize) { + #[cfg(feature = "flutter")] + pub fn refresh_video(&self, display: i32) { if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) { - self.send(Data::Message(LoginConfigHandler::refresh_display(display))); + self.send(Data::Message(LoginConfigHandler::refresh_display(display as _))); } else { self.send(Data::Message(LoginConfigHandler::refresh())); } } + #[cfg(not(feature = "flutter"))] + pub fn refresh_video(&self, _display: i32) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + pub fn record_screen(&self, start: bool, display: i32, w: i32, h: i32) { self.send(Data::RecordScreen( start, From c10fc26ccea2901680d76e6f53a6d45d75fcecb8 Mon Sep 17 00:00:00 2001 From: dignow Date: Sun, 8 Oct 2023 23:59:18 +0800 Subject: [PATCH 04/10] fix build, android Signed-off-by: dignow --- src/flutter_ffi.rs | 4 ++-- src/keyboard.rs | 1 + src/platform/mod.rs | 1 + src/ui_session_interface.rs | 9 ++++++--- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 77da2942c..93c7ced7e 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1563,10 +1563,10 @@ pub fn set_cur_session_id(session_id: SessionID) { } } -fn set_cur_session_id_(session_id: SessionID, keyboard_mode: &str) { +fn set_cur_session_id_(session_id: SessionID, _keyboard_mode: &str) { super::flutter::set_cur_session_id(session_id); #[cfg(windows)] - crate::keyboard::update_grab_get_key_name(keyboard_mode); + crate::keyboard::update_grab_get_key_name(_keyboard_mode); } pub fn install_show_run_without_install() -> SyncReturn { diff --git a/src/keyboard.rs b/src/keyboard.rs index 5a6c4bace..9a6ac49d5 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -214,6 +214,7 @@ static mut IS_0X021D_DOWN: bool = false; #[cfg(target_os = "macos")] static mut IS_LEFT_OPTION_DOWN: bool = false; +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn get_keyboard_mode() -> String { #[cfg(not(any(feature = "flutter", feature = "cli")))] if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 6a428d9c3..71aefe817 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -83,6 +83,7 @@ pub const PA_SAMPLE_RATE: u32 = 48000; pub(crate) struct InstallingService; // please use new impl InstallingService { + #[cfg(any(target_os = "windows", target_os = "linux"))] pub fn new() -> Self { *INSTALLING_SERVICE.lock().unwrap() = true; Self diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index ff559f19c..568767f81 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -3,11 +3,12 @@ use async_trait::async_trait; use bytes::Bytes; use rdev::{Event, EventType::*, KeyCode}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use std::{collections::HashMap, sync::atomic::AtomicBool}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::{ + collections::HashMap, ops::{Deref, DerefMut}, str::FromStr, - sync::{atomic::Ordering, Arc, Mutex, RwLock}, + sync::{Arc, Mutex, RwLock}, time::SystemTime, }; use uuid::Uuid; @@ -285,7 +286,9 @@ impl Session { #[cfg(feature = "flutter")] pub fn refresh_video(&self, display: i32) { if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) { - self.send(Data::Message(LoginConfigHandler::refresh_display(display as _))); + self.send(Data::Message(LoginConfigHandler::refresh_display( + display as _, + ))); } else { self.send(Data::Message(LoginConfigHandler::refresh())); } From e363cd98131da94b6b7cf1230c884cb411c43007 Mon Sep 17 00:00:00 2001 From: dignow Date: Mon, 9 Oct 2023 14:21:17 +0800 Subject: [PATCH 05/10] 'Choose Display Behavior' to 'Choose display behavior' Signed-off-by: dignow --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- src/lang/ar.rs | 2 +- src/lang/ca.rs | 2 +- src/lang/cn.rs | 2 +- src/lang/cs.rs | 2 +- src/lang/da.rs | 2 +- src/lang/de.rs | 2 +- src/lang/el.rs | 2 +- src/lang/eo.rs | 2 +- src/lang/es.rs | 2 +- src/lang/fa.rs | 2 +- src/lang/fr.rs | 2 +- src/lang/hu.rs | 2 +- src/lang/id.rs | 2 +- src/lang/it.rs | 2 +- src/lang/ja.rs | 2 +- src/lang/ko.rs | 2 +- src/lang/kz.rs | 2 +- src/lang/lt.rs | 2 +- src/lang/lv.rs | 2 +- src/lang/pl.rs | 2 +- src/lang/pt_PT.rs | 2 +- src/lang/ptbr.rs | 2 +- src/lang/ro.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/sl.rs | 2 +- src/lang/sq.rs | 2 +- src/lang/sr.rs | 2 +- src/lang/sv.rs | 2 +- src/lang/template.rs | 2 +- src/lang/th.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/ua.rs | 2 +- src/lang/vn.rs | 2 +- 36 files changed, 36 insertions(+), 36 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 9daaea9f6..84c941c88 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1142,7 +1142,7 @@ class _DisplayState extends State<_Display> { setState(() {}); } - return _Card(title: 'Choose Display Behavior', children: [ + return _Card(title: 'Choose display behavior', children: [ _Radio(context, value: kChooseDisplayBehaviorSwitch, groupValue: current, diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 99bb7c3e0..90252faa7 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ca.rs b/src/lang/ca.rs index b4938f8e5..90887b622 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 15a74e11c..3793c2e5d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", "显示器被拔出,切换到第一个显示器。"), ("No displays", "没有显示器。"), ("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"), - ("Choose Display Behavior", "选择显示器的行为"), + ("Choose display behavior", "选择显示器的行为"), ("Switch display", "切换显示器"), ("Open in new window", "在新的窗口中打开"), ].iter().cloned().collect(); diff --git a/src/lang/cs.rs b/src/lang/cs.rs index df57c1d9e..71e152668 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/da.rs b/src/lang/da.rs index 6f9092230..f11954d02 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/de.rs b/src/lang/de.rs index 55f7b2dd0..dc80bad04 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/el.rs b/src/lang/el.rs index 597829043..b1baabb2b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 03692ff77..0ce93fdff 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/es.rs b/src/lang/es.rs index de1ec9fed..aa040d252 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 07829de99..b7561283c 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 810dbddc1..f0d3c1d8f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/hu.rs b/src/lang/hu.rs index ce8cb5add..5a5e280d6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/id.rs b/src/lang/id.rs index f1f18ca5c..cf3702176 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/it.rs b/src/lang/it.rs index be1157776..fb84fbbdc 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -563,7 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 43daac2de..7f85fcbb0 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 94ad160f8..1cce7aa7d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 81003b1b2..3f52f8c7c 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/lt.rs b/src/lang/lt.rs index d77e543ac..f1e4e790f 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/lv.rs b/src/lang/lv.rs index d10238608..44c53889f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 4f0fe343c..beb47a4ec 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index e794b9996..b0df8eefa 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a75da7507..ceeb1aa22 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ro.rs b/src/lang/ro.rs index f03d0b96d..b194edd25 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 302a4e209..b132d88b4 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/sk.rs b/src/lang/sk.rs index be35fa666..55ab7fb64 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 311106d74..13440aa70 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 31a2096bf..718f00518 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/sr.rs b/src/lang/sr.rs index dc11a8af0..755c52784 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/sv.rs b/src/lang/sv.rs index ffeaea2fc..80a7098fc 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/template.rs b/src/lang/template.rs index a62790a5d..ae4885298 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/th.rs b/src/lang/th.rs index eb9dd789f..a6f10fb4f 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/tr.rs b/src/lang/tr.rs index acf67740d..abc1d4515 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a89a4b547..cd0da2655 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 5e10c9383..ce5561d52 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 860cd2dbd..d77125ce3 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -562,7 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), + ("Choose display behavior", ""), ("Switch display", ""), ("Open in new window", ""), ].iter().cloned().collect(); From b52cf070f5e95c906640dc02a42e63f944174a8e Mon Sep 17 00:00:00 2001 From: dignow Date: Mon, 9 Oct 2023 17:22:22 +0800 Subject: [PATCH 06/10] multi flutter ui sessions, refact 'Show displays as individual windows' Signed-off-by: dignow --- flutter/lib/common.dart | 15 +++------ flutter/lib/common/widgets/toolbar.dart | 18 ++++++++++ flutter/lib/consts.dart | 5 ++- .../desktop/pages/desktop_setting_page.dart | 29 ++-------------- .../lib/desktop/widgets/remote_toolbar.dart | 33 +++++++++++++++++-- flutter/lib/models/model.dart | 5 ++- libs/hbb_common/src/config.rs | 12 +++++++ src/client.rs | 11 +++++++ src/flutter_ffi.rs | 14 ++++++++ src/lang/ar.rs | 3 +- src/lang/ca.rs | 3 +- src/lang/cn.rs | 3 +- src/lang/cs.rs | 3 +- src/lang/da.rs | 3 +- src/lang/de.rs | 3 +- src/lang/el.rs | 3 +- src/lang/eo.rs | 3 +- src/lang/es.rs | 3 +- src/lang/fa.rs | 3 +- src/lang/fr.rs | 3 +- src/lang/hu.rs | 3 +- src/lang/id.rs | 3 +- src/lang/it.rs | 3 +- src/lang/ja.rs | 3 +- src/lang/ko.rs | 3 +- src/lang/kz.rs | 3 +- src/lang/lt.rs | 3 +- src/lang/lv.rs | 3 +- src/lang/nl.rs | 3 +- src/lang/pl.rs | 3 +- src/lang/pt_PT.rs | 3 +- src/lang/ptbr.rs | 3 +- src/lang/ro.rs | 3 +- src/lang/ru.rs | 3 +- src/lang/sk.rs | 3 +- src/lang/sl.rs | 3 +- src/lang/sq.rs | 3 +- src/lang/sr.rs | 3 +- src/lang/sv.rs | 3 +- src/lang/template.rs | 3 +- src/lang/th.rs | 3 +- src/lang/tr.rs | 3 +- src/lang/tw.rs | 3 +- src/lang/ua.rs | 3 +- src/lang/vn.rs | 3 +- src/ui_session_interface.rs | 8 +++++ 46 files changed, 140 insertions(+), 118 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 61d006b8a..73b4bf1a5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2590,15 +2590,6 @@ String getDesktopTabLabel(String peerId, String alias) { return label; } -String getChooseDisplayBehavior() { - var current = bind.mainGetOptionSync(key: kKeyChooseDisplayBehavior); - if (![kChooseDisplayBehaviorSwitch, kChooseDisplayBehaviorOpen] - .contains(current)) { - current = kChooseDisplayBehaviorOpen; - } - return current; -} - sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async { if (pi.currentDisplay == kAllDisplayValue) { for (int i = 0; i < pi.displays.length; i++) { @@ -2609,5 +2600,7 @@ sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async { } } -bool get isChooseDisplayToOpen => - getChooseDisplayBehavior() != kChooseDisplayBehaviorSwitch; +bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) => + pi.isSupportMultiDisplay && + bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y'; + diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 1717e99f2..ceb28b0a3 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -515,5 +515,23 @@ Future> toolbarDisplayToggle( }, child: Text(translate('Swap control-command key')))); } + + if (pi.isSupportMultiDisplay && + PrivacyModeState.find(id).isFalse && + pi.displaysCount.value > 1 && + bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { + final value = + bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) == + 'Y'; + v.add(TToggleMenu( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionSetDisplaysAsIndividualWindows( + sessionId: sessionId, value: value ? 'Y' : ''); + }, + child: Text(translate('Show displays as individual windows')))); + } + return v; } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 9c7fdf01d..1452270d1 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -66,9 +66,8 @@ const int kWindowMainId = 0; const String kPointerEventKindTouch = "touch"; const String kPointerEventKindMouse = "mouse"; -const String kKeyChooseDisplayBehavior = 'choose-display-behavior'; -const String kChooseDisplayBehaviorSwitch = 'switch'; -const String kChooseDisplayBehaviorOpen = 'open'; +const String kKeyShowDisplaysAsIndividualWindows = 'displays_as_individual_windows'; +const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar'; // the executable name of the portable version const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 84c941c88..259511bfd 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -11,7 +11,6 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; -import 'package:flutter_hbb/models/desktop_render_texture.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; @@ -1124,7 +1123,6 @@ class _DisplayState extends State<_Display> { controller: scrollController, physics: DraggableNeverScrollableScrollPhysics(), children: [ - chooseDisplay(context), viewStyle(context), scrollStyle(context), imageQuality(context), @@ -1133,29 +1131,6 @@ class _DisplayState extends State<_Display> { ]).marginOnly(bottom: _kListViewBottomMargin)); } - Widget chooseDisplay(BuildContext context) { - if (!useTextureRender) return const Offstage(); - - var current = getChooseDisplayBehavior(); - onChanged(String value) { - bind.mainSetOption(key: kKeyChooseDisplayBehavior, value: value); - setState(() {}); - } - - return _Card(title: 'Choose display behavior', children: [ - _Radio(context, - value: kChooseDisplayBehaviorSwitch, - groupValue: current, - label: 'Switch display', - onChanged: onChanged), - _Radio(context, - value: kChooseDisplayBehaviorOpen, - groupValue: current, - label: 'Open in new window', - onChanged: onChanged), - ]); - } - Widget viewStyle(BuildContext context) { final key = 'view_style'; onChanged(String value) async { @@ -1314,7 +1289,7 @@ class _DisplayState extends State<_Display> { Widget other(BuildContext context) { return _Card(title: 'Other Default Options', children: [ otherRow('View Mode', 'view_only'), - otherRow('show_monitors_tip', 'show_monitors_toolbar'), + otherRow('show_monitors_tip', kKeyShowMonitorsToolbar), otherRow('Collapse toolbar', 'collapse_toolbar'), otherRow('Show remote cursor', 'show_remote_cursor'), otherRow('Zoom cursor', 'zoom-cursor'), @@ -1325,6 +1300,8 @@ class _DisplayState extends State<_Display> { otherRow('Lock after session end', 'lock_after_session_end'), otherRow('Privacy mode', 'privacy_mode'), otherRow('Reverse mouse wheel', 'reverse_mouse_wheel'), + otherRow('Show displays as individual windows', + kKeyShowDisplaysAsIndividualWindows), ]); } } diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index dddbc32a1..1bf4d438a 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -598,7 +598,7 @@ class _MonitorMenu extends StatelessWidget { }) : super(key: key); bool get showMonitorsToolbar => - bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y'; + bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y'; @override Widget build(BuildContext context) => @@ -614,13 +614,40 @@ class _MonitorMenu extends StatelessWidget { menuStyle: MenuStyle( padding: MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), - menuChildren: [Row(children: buildMonitorList(false))]); + menuChildren: [buildMonitorSubmenuWidget()]); } Widget buildMultiMonitorMenu() { return Row(children: buildMonitorList(true)); } + Widget buildMonitorSubmenuWidget() { + final pi = ffi.ffiModel.pi; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row(children: buildMonitorList(false)), + pi.isSupportMultiDisplay ? Divider() : Offstage(), + pi.isSupportMultiDisplay ? chooseDisplayBehavior() : Offstage(), + ], + ); + } + + Widget chooseDisplayBehavior() { + final value = + bind.sessionGetDisplaysAsIndividualWindows(sessionId: ffi.sessionId) == + 'Y'; + return CkbMenuButton( + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionSetDisplaysAsIndividualWindows( + sessionId: ffi.sessionId, value: value ? 'Y' : ''); + }, + ffi: ffi, + child: Text(translate('Show displays as individual windows'))); + } + List buildMonitorList(bool isMulti) { final List monitorList = []; final pi = ffi.ffiModel.pi; @@ -746,7 +773,7 @@ class _MonitorMenu extends StatelessWidget { _menuDismissCallback(ffi); RxInt display = CurrentDisplayState.find(id); if (display.value != i) { - if (pi.isSupportMultiDisplay) { + if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) { openMonitorInNewTabOrWindow(i, pi); } else { openMonitorInTheSameTab(i, pi); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7bb96a99b..a0fde0ca4 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -430,7 +430,7 @@ class FfiModel with ChangeNotifier { final curDisplay = int.parse(evt['display']); // The message should be handled by the another UI session. - if (_pi.isSupportMultiDisplay) { + if (isChooseDisplayToOpenInNewWindow(_pi, sessionId)) { if (curDisplay != _pi.currentDisplay) { return; } @@ -2211,8 +2211,7 @@ class PeerInfo with ChangeNotifier { bool get isWayland => platformDdditions['is_wayland'] == true; bool get isHeadless => platformDdditions['headless'] == true; - bool get isSupportMultiDisplay => - isDesktop && isSupportMultiUiSession && isChooseDisplayToOpen; + bool get isSupportMultiDisplay => isDesktop && isSupportMultiUiSession; bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false; diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 5a7c7d4b4..02f3c719e 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -284,6 +284,12 @@ pub struct PeerConfig { skip_serializing_if = "String::is_empty" )] pub reverse_mouse_wheel: String, + #[serde( + default = "PeerConfig::default_displays_as_individual_windows", + deserialize_with = "PeerConfig::deserialize_displays_as_individual_windows", + skip_serializing_if = "String::is_empty" + )] + pub displays_as_individual_windows: String, #[serde( default, @@ -328,6 +334,7 @@ impl Default for PeerConfig { keyboard_mode: Default::default(), view_only: Default::default(), reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), + displays_as_individual_windows: Self::default_displays_as_individual_windows(), custom_resolutions: Default::default(), options: Self::default_options(), ui_flutter: Default::default(), @@ -1144,6 +1151,11 @@ impl PeerConfig { deserialize_reverse_mouse_wheel, UserDefaultConfig::read().get("reverse_mouse_wheel") ); + serde_field_string!( + default_displays_as_individual_windows, + deserialize_displays_as_individual_windows, + UserDefaultConfig::read().get("displays_as_individual_windows") + ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::read() diff --git a/src/client.rs b/src/client.rs index eff490ebb..445aaf225 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1207,6 +1207,17 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save reverse mouse wheel ("", "Y") to the current config. + /// + /// # Arguments + /// + /// * `value` - The "displays_as_individual_windows" value ("", "Y"). + pub fn save_displays_as_individual_windows(&mut self, value: String) { + let mut config = self.load_config(); + config.displays_as_individual_windows = value; + self.save_config(config); + } + /// Save scroll style to the current config. /// /// # Arguments diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 93c7ced7e..e5f5d6573 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -339,6 +339,20 @@ pub fn session_set_reverse_mouse_wheel(session_id: SessionID, value: String) { } } +pub fn session_get_displays_as_individual_windows(session_id: SessionID) -> SyncReturn> { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + SyncReturn(Some(session.get_displays_as_individual_windows())) + } else { + SyncReturn(None) + } +} + +pub fn session_set_displays_as_individual_windows(session_id: SessionID, value: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_displays_as_individual_windows(value); + } +} + pub fn session_get_custom_image_quality(session_id: SessionID) -> Option> { if let Some(session) = sessions::get_session_by_session_id(&session_id) { Some(session.get_custom_image_quality()) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 90252faa7..e30621c21 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 90887b622..e70c2c044 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 3793c2e5d..ba2796157 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", "显示器被拔出,切换到第一个显示器。"), ("No displays", "没有显示器。"), ("elevated_switch_display_msg", "切换到主显示器,因为提权后,不支持多显示器画面。"), - ("Choose display behavior", "选择显示器的行为"), - ("Switch display", "切换显示器"), ("Open in new window", "在新的窗口中打开"), + ("Show displays as individual windows", "在单个窗口中打开显示器"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 71e152668..8907e78e0 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index f11954d02..9d76b8e2e 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index dc80bad04..d051700b0 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index b1baabb2b..472b494e7 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0ce93fdff..ccc674749 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index aa040d252..1b1b83afa 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index b7561283c..ff90b0430 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index f0d3c1d8f..2195d4fdd 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5a5e280d6..24ac09d05 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index cf3702176..09e7853e4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index fb84fbbdc..4416508bd 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -563,8 +563,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 7f85fcbb0..eeed3e147 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1cce7aa7d..dcf28718a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 3f52f8c7c..43c36e32c 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index f1e4e790f..02d4714c2 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 44c53889f..8de908d9c 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index f5a1d5322..71d1c1107 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose Display Behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index beb47a4ec..c8b2a4cc3 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index b0df8eefa..85863d2ae 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index ceeb1aa22..be4cdfd8e 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index b194edd25..311a13bec 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b132d88b4..e05322b88 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 55ab7fb64..23751b734 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 13440aa70..8f84b181a 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 718f00518..3d26b5635 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 755c52784..03f4776b7 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 80a7098fc..ce2dab684 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ae4885298..04d3d53ef 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a6f10fb4f..75afe0324 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index abc1d4515..e51cff3db 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index cd0da2655..c70cf8f35 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index ce5561d52..94e0bc423 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index d77125ce3..895e7491a 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -562,8 +562,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("display_is_plugged_out_msg", ""), ("No displays", ""), ("elevated_switch_display_msg", ""), - ("Choose display behavior", ""), - ("Switch display", ""), ("Open in new window", ""), + ("Show displays as individual windows", ""), ].iter().cloned().collect(); } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 568767f81..0eea85173 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -236,10 +236,18 @@ impl Session { self.lc.read().unwrap().reverse_mouse_wheel.clone() } + pub fn get_displays_as_individual_windows(&self) -> String { + self.lc.read().unwrap().displays_as_individual_windows.clone() + } + pub fn save_reverse_mouse_wheel(&self, value: String) { self.lc.write().unwrap().save_reverse_mouse_wheel(value); } + pub fn save_displays_as_individual_windows(&self, value: String) { + self.lc.write().unwrap().save_displays_as_individual_windows(value); + } + pub fn save_view_style(&self, value: String) { self.lc.write().unwrap().save_view_style(value); } From c13d67dea52bfb511c899fe84533cd94feaf3ef9 Mon Sep 17 00:00:00 2001 From: SergeyMy <131688106+SergeyMy@users.noreply.github.com> Date: Sat, 14 Oct 2023 09:45:06 +0500 Subject: [PATCH 07/10] Update ru.rs --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 22e920b8e..8dcc132df 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -556,7 +556,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_rustdesk_server_pro_to_{}_tip", "Обновите RustDesk Server Pro до версии {} или новее!"), ("pull_group_failed_tip", "Невозможно обновить группу"), ("Filter by intersection", "Фильтровать по пересечению"), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Remove wallpaper during incoming sessions", "Удалить обои в сеансе"), + ("Test", "Тест"), ].iter().cloned().collect(); } From 68ef1fc9e0a07f3160216044046814e0c5a0bb99 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 14 Oct 2023 18:50:41 +0800 Subject: [PATCH 08/10] show wallpaper only when support, show test on checked Signed-off-by: 21pages --- Cargo.lock | 2 +- .../desktop/pages/desktop_setting_page.dart | 55 +++++++++++++------ src/flutter_ffi.rs | 4 ++ src/platform/linux.rs | 8 +++ src/platform/windows.rs | 4 ++ src/ui.rs | 5 ++ src/ui/index.tis | 3 +- src/ui_interface.rs | 15 ++++- 8 files changed, 75 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca9ddc2f4..519476e93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6602,7 +6602,7 @@ dependencies = [ [[package]] name = "wallpaper" version = "3.2.0" -source = "git+https://github.com/21pages/wallpaper.rs#2bbb70acd93be179c69cb96cb8c3dda487e6f5fd" +source = "git+https://github.com/21pages/wallpaper.rs#ce4a0cd3f58327c7cc44d15a63706fb0c022bacf" dependencies = [ "dirs 5.0.1", "enquote", diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a105f2135..3bbade76e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -323,24 +323,7 @@ class _GeneralState extends State<_General> { 'enable-confirm-closing-tabs', isServer: false), _OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'), - if (Platform.isWindows || Platform.isLinux) - Row( - children: [ - Flexible( - child: _OptionCheckBox( - context, - 'Remove wallpaper during incoming sessions', - 'allow-remove-wallpaper'), - ), - _CountDownButton( - text: 'Test', - second: 5, - onPressed: () { - bind.mainTestWallpaper(second: 5); - }, - ) - ], - ), + wallpaper(), _OptionCheckBox( context, 'Open connection in new tab', @@ -367,6 +350,42 @@ class _GeneralState extends State<_General> { return _Card(title: 'Other', children: children); } + Widget wallpaper() { + return futureBuilder(future: () async { + final support = await bind.mainSupportRemoveWallpaper(); + return support; + }(), hasData: (data) { + if (data is bool && data == true) { + final option = 'allow-remove-wallpaper'; + bool value = mainGetBoolOptionSync(option); + return Row( + children: [ + Flexible( + child: _OptionCheckBox( + context, + 'Remove wallpaper during incoming sessions', + option, + update: () { + setState(() {}); + }, + ), + ), + if (value) + _CountDownButton( + text: 'Test', + second: 5, + onPressed: () { + bind.mainTestWallpaper(second: 5); + }, + ) + ], + ); + } + + return Offstage(); + }); + } + Widget hwcodec() { return Offstage( offstage: !bind.mainHasHwcodec(), diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index b4ae89bc9..883835c26 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1625,6 +1625,10 @@ pub fn main_test_wallpaper(_second: u64) { }); } +pub fn main_support_remove_wallpaper() -> bool { + support_remove_wallpaper() +} + /// Send a url scheme throught the ipc. /// /// * macOS only diff --git a/src/platform/linux.rs b/src/platform/linux.rs index a4975d3aa..37b27cf64 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1341,6 +1341,14 @@ impl WallPaperRemover { old_path_dark, }) } + + pub fn support() -> bool { + let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + if wallpaper::gnome::is_compliant(&desktop) || desktop.as_str() == "XFCE" { + return wallpaper::get().is_ok(); + } + false + } } impl Drop for WallPaperRemover { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 9cc4fd39f..f664b1aee 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2392,6 +2392,10 @@ impl WallPaperRemover { Ok(Self { old_path }) } + pub fn support() -> bool { + wallpaper::get().is_ok() || !Self::get_recent_wallpaper().unwrap_or_default().is_empty() + } + fn get_recent_wallpaper() -> ResultType { // SystemParametersInfoW may return %appdata%\Microsoft\Windows\Themes\TranscodedWallpaper, not real path and may not real cache // https://www.makeuseof.com/find-desktop-wallpapers-file-location-windows-11/ diff --git a/src/ui.rs b/src/ui.rs index 27793b31f..fbe715fa9 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -598,6 +598,10 @@ impl UI { fn get_login_device_info(&self) -> String { get_login_device_info_json() } + + fn support_remove_wallpaper(&self) -> bool { + support_remove_wallpaper() + } } impl sciter::EventHandler for UI { @@ -683,6 +687,7 @@ impl sciter::EventHandler for UI { fn default_video_save_directory(); fn handle_relay_id(String); fn get_login_device_info(); + fn support_remove_wallpaper(); } } diff --git a/src/ui/index.tis b/src/ui/index.tis index 67deed7d7..7a46e3b3f 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -210,6 +210,7 @@ class Enhancements: Reactor.Component { function render() { var has_hwcodec = handler.has_hwcodec(); + var support_remove_wallpaper = handler.support_remove_wallpaper(); var me = this; self.timer(1ms, function() { me.toggleMenuState() }); return
  • {translate('Enhancements')} @@ -217,7 +218,7 @@ class Enhancements: Reactor.Component { {has_hwcodec ?
  • {svg_checkmark}{translate("Hardware Codec")} (beta)
  • : ""}
  • {svg_checkmark}{translate("Adaptive bitrate")} (beta)
  • {translate("Recording")}
  • - {is_osx ? "" :
  • {svg_checkmark}{translate("Remove wallpaper during incoming sessions")}
  • } + {support_remove_wallpaper ?
  • {svg_checkmark}{translate("Remove wallpaper during incoming sessions")}
  • : ""} ; } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index ed2b4f4fc..f61e683ed 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -594,7 +594,13 @@ pub fn current_is_wayland() -> bool { #[inline] pub fn get_new_version() -> String { - (*SOFTWARE_UPDATE_URL.lock().unwrap().rsplit('/').next().unwrap_or("")).to_string() + (*SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .rsplit('/') + .next() + .unwrap_or("")) + .to_string() } #[inline] @@ -1248,3 +1254,10 @@ pub fn handle_relay_id(id: String) -> String { id } } + +pub fn support_remove_wallpaper() -> bool { + #[cfg(any(target_os = "windows", target_os = "linux"))] + return crate::platform::WallPaperRemover::support(); + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + return false; +} From 3070b0019edb05331c817a993e5188f7b3ae01f4 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:01:09 +0200 Subject: [PATCH 09/10] Update de.rs @grummbeer You are right. --- src/lang/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index aac437388..0eac4fd6a 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -557,6 +557,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), ("Remove wallpaper during incoming sessions", ""), ("Test", ""), - ("Filter by intersection", "Nach Schnittpunkt filtern") + ("Filter by intersection", "Nach Schnittmenge filtern") ].iter().cloned().collect(); } From 8a2ab30302b4ddbf13f5e41e056605001fbabb31 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:07:02 +0200 Subject: [PATCH 10/10] Update de.rs @grummbeer You are right. --- src/lang/de.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 0eac4fd6a..159a121cf 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -555,8 +555,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Check for software update on startup", "Beim Start auf Softwareaktualisierung prüfen"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Bitte aktualisieren Sie RustDesk Server Pro auf die Version {} oder neuer!"), ("pull_group_failed_tip", "Aktualisierung der Gruppe fehlgeschlagen"), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("Filter by intersection", "Nach Schnittmenge filtern") + ("Filter by intersection", "Nach Schnittmenge filtern"), + ("Remove wallpaper during incoming sessions", "Hintergrundbild während eingehender Sitzungen entfernen"), + ("Test", "Test"), ].iter().cloned().collect(); }