diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 3f6bb7d16..d709d3c52 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -19,8 +19,6 @@ import '../../common/widgets/peer_tab_page.dart'; import '../../models/platform_model.dart'; import '../widgets/button.dart'; -import 'package:flutter_hbb/common/widgets/dialog.dart'; - /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { const ConnectionPage({Key? key}) : super(key: key); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index b10d3245f..bba146fa8 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -660,69 +660,24 @@ class _ControlMenu extends StatelessWidget { } } -class _DisplayMenu extends StatefulWidget { +class ScreenAdjustor { final String id; final FFI ffi; - final MenubarState state; - final Function(bool) setFullscreen; - final Widget pluginItem; - _DisplayMenu( - {Key? key, - required this.id, - required this.ffi, - required this.state, - required this.setFullscreen}) - : pluginItem = LocationItem.createLocationItem( - id, - ffi, - kLocationClientRemoteToolbarDisplay, - true, - ), - super(key: key); - - @override - State<_DisplayMenu> createState() => _DisplayMenuState(); -} - -class _DisplayMenuState extends State<_DisplayMenu> { + final VoidCallback cbExitFullscreen; window_size.Screen? _screen; + ScreenAdjustor({ + required this.id, + required this.ffi, + required this.cbExitFullscreen, + }); + bool get isFullscreen => stateGlobal.fullscreen; - int get windowId => stateGlobal.windowId; - Map get perms => widget.ffi.ffiModel.permissions; - - PeerInfo get pi => widget.ffi.ffiModel.pi; - FfiModel get ffiModel => widget.ffi.ffiModel; - FFI get ffi => widget.ffi; - String get id => widget.id; - - @override - Widget build(BuildContext context) { - _updateScreen(); - return _IconSubmenuButton( - tooltip: 'Display Settings', - svg: "assets/display.svg", - ffi: widget.ffi, - color: _MenubarTheme.blueColor, - hoverColor: _MenubarTheme.hoverBlueColor, - menuChildren: [ - adjustWindow(), - viewStyle(), - scrollStyle(), - imageQuality(), - codec(), - resolutions(), - Divider(), - toggles(), - widget.pluginItem, - ]); - } - adjustWindow() { return futureBuilder( - future: _isWindowCanBeAdjusted(), + future: isWindowCanBeAdjusted(), hasData: (data) { final visible = data as bool; if (!visible) return Offstage(); @@ -730,18 +685,18 @@ class _DisplayMenuState extends State<_DisplayMenu> { children: [ MenuButton( child: Text(translate('Adjust Window')), - onPressed: _doAdjustWindow, - ffi: widget.ffi), + onPressed: doAdjustWindow, + ffi: ffi), Divider(), ], ); }); } - _doAdjustWindow() async { - await _updateScreen(); + doAdjustWindow() async { + await updateScreen(); if (_screen != null) { - widget.setFullscreen(false); + cbExitFullscreen(); double scale = _screen!.scaleFactor; final wndRect = await WindowController.fromWindowId(windowId).getFrame(); final mediaSize = MediaQueryData.fromWindow(ui.window).size; @@ -752,7 +707,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { double magicHeight = wndRect.bottom - wndRect.top - mediaSize.height * scale; - final canvasModel = widget.ffi.canvasModel; + final canvasModel = ffi.canvasModel; final width = (canvasModel.getDisplayWidth() * canvasModel.scale + CanvasModel.leftToEdge + CanvasModel.rightToEdge) * @@ -787,7 +742,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - _updateScreen() async { + updateScreen() async { final v = await rustDeskWinManager.call( WindowType.Main, kWindowGetWindowInfo, ''); final String valueStr = v; @@ -807,8 +762,8 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - Future _isWindowCanBeAdjusted() async { - final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + Future isWindowCanBeAdjusted() async { + final viewStyle = await bind.sessionGetViewStyle(id: id) ?? ''; if (viewStyle != kRemoteViewStyleOriginal) { return false; } @@ -827,7 +782,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { selfHeight = _screen!.frame.height; } - final canvasModel = widget.ffi.canvasModel; + final canvasModel = ffi.canvasModel; final displayWidth = canvasModel.getDisplayWidth(); final displayHeight = canvasModel.getDisplayHeight(); final requiredWidth = @@ -837,6 +792,77 @@ class _DisplayMenuState extends State<_DisplayMenu> { return selfWidth > (requiredWidth * scale) && selfHeight > (requiredHeight * scale); } +} + +class _DisplayMenu extends StatefulWidget { + final String id; + final FFI ffi; + final MenubarState state; + final Function(bool) setFullscreen; + final Widget pluginItem; + _DisplayMenu( + {Key? key, + required this.id, + required this.ffi, + required this.state, + required this.setFullscreen}) + : pluginItem = LocationItem.createLocationItem( + id, + ffi, + kLocationClientRemoteToolbarDisplay, + true, + ), + super(key: key); + + @override + State<_DisplayMenu> createState() => _DisplayMenuState(); +} + +class _DisplayMenuState extends State<_DisplayMenu> { + late final ScreenAdjustor _screenAdjustor = ScreenAdjustor( + id: widget.id, + ffi: widget.ffi, + cbExitFullscreen: () => widget.setFullscreen(false), + ); + + bool get isFullscreen => stateGlobal.fullscreen; + int get windowId => stateGlobal.windowId; + Map get perms => widget.ffi.ffiModel.permissions; + PeerInfo get pi => widget.ffi.ffiModel.pi; + FfiModel get ffiModel => widget.ffi.ffiModel; + FFI get ffi => widget.ffi; + String get id => widget.id; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + _screenAdjustor.updateScreen(); + return _IconSubmenuButton( + tooltip: 'Display Settings', + svg: "assets/display.svg", + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [ + _screenAdjustor.adjustWindow(), + viewStyle(), + scrollStyle(), + imageQuality(), + codec(), + _ResolutionsMenu( + id: widget.id, + ffi: widget.ffi, + screenAdjustor: _screenAdjustor, + ), + Divider(), + toggles(), + widget.pluginItem, + ]); + } viewStyle() { return futureBuilder( @@ -935,46 +961,6 @@ class _DisplayMenuState extends State<_DisplayMenu> { }); } - resolutions() { - final resolutions = pi.resolutions; - final visible = ffiModel.keyboard && resolutions.length > 1; - if (!visible) return Offstage(); - final display = ffiModel.display; - final groupValue = "${display.width}x${display.height}"; - onChanged(String? value) async { - if (value == null) return; - final list = value.split('x'); - if (list.length == 2) { - final w = int.tryParse(list[0]); - final h = int.tryParse(list[1]); - if (w != null && h != null) { - await bind.sessionChangeResolution( - id: widget.id, width: w, height: h); - Future.delayed(Duration(seconds: 3), () async { - final display = ffiModel.display; - if (w == display.width && h == display.height) { - if (await _isWindowCanBeAdjusted()) { - _doAdjustWindow(); - } - } - }); - } - } - } - - return _SubmenuButton( - ffi: widget.ffi, - menuChildren: resolutions - .map((e) => RdoMenuButton( - value: '${e.width}x${e.height}', - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - child: Text('${e.width}x${e.height}'))) - .toList(), - child: Text(translate("Resolution"))); - } - toggles() { return futureBuilder( future: toolbarDisplayToggle(context, id, ffi), @@ -993,6 +979,192 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } +class _ResolutionsMenu extends StatefulWidget { + final String id; + final FFI ffi; + final ScreenAdjustor screenAdjustor; + + _ResolutionsMenu({ + Key? key, + required this.id, + required this.ffi, + required this.screenAdjustor, + }) : super(key: key); + + @override + State<_ResolutionsMenu> createState() => _ResolutionsMenuState(); +} + +class _ResolutionsMenuState extends State<_ResolutionsMenu> { + String _groupValue = ''; + Resolution? _localResolution; + + PeerInfo get pi => widget.ffi.ffiModel.pi; + FfiModel get ffiModel => widget.ffi.ffiModel; + Display get display => ffiModel.display; + List get resolutions => pi.resolutions; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final isVirtualDisplay = display.isVirtualDisplayResolution; + // final visible = + // ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1); + final visible = ffiModel.keyboard && resolutions.length > 1; + if (!visible) return Offstage(); + _groupValue = '${display.width}x${display.height}'; + _getLocalResolution(); + final showOriginalBtn = + display.isOriginalResolutionSet && !display.isOriginalResolution; + final showFitLocalBtn = !_isRemoteResolutionFitLocal(); + + return _SubmenuButton( + ffi: widget.ffi, + menuChildren: [ + _OriginalResolutionMenuButton(showOriginalBtn), + _FitLocalResolutionMenuButton(showFitLocalBtn), + // _customResolutionMenuButton(isVirtualDisplay), + _menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay), + ] + + _supportedResolutionMenuButtons(), + child: Text(translate("Resolution")), + ); + } + + _menuDivider( + bool showOriginalBtn, bool showFitLocalBtn, bool isVirtualDisplay) { + return Offstage( + // offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay), + offstage: !(showOriginalBtn || showFitLocalBtn), + child: Divider(), + ); + } + + _getLocalResolution() { + _localResolution = null; + final String currentDisplay = bind.mainGetCurrentDisplay(); + if (currentDisplay.isNotEmpty) { + try { + final display = json.decode(currentDisplay); + if (display['w'] != null && display['h'] != null) { + _localResolution = Resolution(display['w'], display['h']); + } + } catch (e) { + debugPrint('Failed to decode $currentDisplay, $e'); + } + } + } + + _onChanged(String? value) async { + if (value == null) return; + final list = value.split('x'); + if (list.length == 2) { + final w = int.tryParse(list[0]); + final h = int.tryParse(list[1]); + if (w != null && h != null) { + await _changeResolution(w, h); + } + } + } + + _changeResolution(int w, int h) async { + await bind.sessionChangeResolution( + id: widget.id, + width: w, + height: h, + ); + Future.delayed(Duration(seconds: 3), () async { + final display = ffiModel.display; + if (w == display.width && h == display.height) { + if (await widget.screenAdjustor.isWindowCanBeAdjusted()) { + widget.screenAdjustor.doAdjustWindow(); + } + } + }); + } + + Widget _OriginalResolutionMenuButton(bool showOriginalBtn) { + return Offstage( + offstage: !showOriginalBtn, + child: MenuButton( + onPressed: () => + _changeResolution(display.originalWidth, display.originalHeight), + ffi: widget.ffi, + child: Text( + '${translate('resolution_original_tip')} ${display.originalWidth}x${display.originalHeight}'), + ), + ); + } + + Widget _FitLocalResolutionMenuButton(bool showFitLocalBtn) { + return Offstage( + offstage: !showFitLocalBtn, + child: MenuButton( + onPressed: () { + final resolution = _getBestFitResolution(); + if (resolution != null) { + _changeResolution(resolution.width, resolution.height); + } + }, + ffi: widget.ffi, + child: Text( + '${translate('resolution_fit_local_tip')} ${_localResolution?.width ?? 0}x${_localResolution?.height ?? 0}'), + ), + ); + } + + List _supportedResolutionMenuButtons() => resolutions + .map((e) => RdoMenuButton( + value: '${e.width}x${e.height}', + groupValue: _groupValue, + onChanged: _onChanged, + ffi: widget.ffi, + child: Text('${e.width}x${e.height}'))) + .toList(); + + Resolution? _getBestFitResolution() { + if (_localResolution == null) { + return null; + } + + if (display.isVirtualDisplayResolution) { + return _localResolution!; + } + + squareDistance(Resolution lhs, Resolution rhs) => + (lhs.width - rhs.width) * (lhs.width - rhs.width) + + (lhs.height - rhs.height) * (lhs.height - rhs.height); + + Resolution res = Resolution(display.width, display.height); + for (final r in resolutions) { + if (r.width <= _localResolution!.width && + r.height <= _localResolution!.height) { + if (squareDistance(r, _localResolution!) < + squareDistance(res, _localResolution!)) { + res = r; + } + } + } + return res; + } + + bool _isRemoteResolutionFitLocal() { + if (_localResolution == null) { + return true; + } + final bestFitResolution = _getBestFitResolution(); + if (bestFitResolution == null) { + return true; + } + return bestFitResolution.width == display.width && + bestFitResolution.height == display.height; + } +} + class _KeyboardMenu extends StatelessWidget { final String id; final FFI ffi; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e94452bd7..294b348b9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -295,11 +295,15 @@ class FfiModel with ChangeNotifier { handleSwitchDisplay(Map evt, String peerId) { _pi.currentDisplay = int.parse(evt['display']); var newDisplay = Display(); - newDisplay.x = double.parse(evt['x']); - newDisplay.y = double.parse(evt['y']); - newDisplay.width = int.parse(evt['width']); - newDisplay.height = int.parse(evt['height']); - newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; + newDisplay.x = double.tryParse(evt['x']) ?? newDisplay.x; + newDisplay.y = double.tryParse(evt['y']) ?? newDisplay.y; + newDisplay.width = int.tryParse(evt['width']) ?? newDisplay.width; + newDisplay.height = int.tryParse(evt['height']) ?? newDisplay.height; + newDisplay.cursorEmbedded = int.tryParse(evt['cursor_embedded']) == 1; + newDisplay.originalWidth = + int.tryParse(evt['original_width']) ?? kInvalidResolutionValue; + newDisplay.originalHeight = + int.tryParse(evt['original_height']) ?? kInvalidResolutionValue; _updateCurDisplay(peerId, newDisplay); @@ -466,14 +470,7 @@ class FfiModel with ChangeNotifier { _pi.displays = []; List displays = json.decode(evt['displays']); for (int i = 0; i < displays.length; ++i) { - Map d0 = displays[i]; - var d = Display(); - d.x = d0['x'].toDouble(); - d.y = d0['y'].toDouble(); - d.width = d0['width']; - d.height = d0['height']; - d.cursorEmbedded = d0['cursor_embedded'] == 1; - _pi.displays.add(d); + _pi.displays.add(evtToDisplay(displays[i])); } stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { @@ -533,20 +530,25 @@ class FfiModel with ChangeNotifier { } } + Display evtToDisplay(Map evt) { + var d = Display(); + d.x = evt['x']?.toDouble() ?? d.x; + d.y = evt['y']?.toDouble() ?? d.y; + d.width = evt['width'] ?? d.width; + d.height = evt['height'] ?? d.height; + d.cursorEmbedded = evt['cursor_embedded'] == 1; + d.originalWidth = evt['original_width'] ?? kInvalidResolutionValue; + d.originalHeight = evt['original_height'] ?? kInvalidResolutionValue; + return d; + } + /// Handle the peer info synchronization event based on [evt]. handleSyncPeerInfo(Map evt, String peerId) async { if (evt['displays'] != null) { List displays = json.decode(evt['displays']); List newDisplays = []; for (int i = 0; i < displays.length; ++i) { - Map d0 = displays[i]; - var d = Display(); - d.x = d0['x'].toDouble(); - d.y = d0['y'].toDouble(); - d.width = d0['width']; - d.height = d0['height']; - d.cursorEmbedded = d0['cursor_embedded'] == 1; - newDisplays.add(d); + newDisplays.add(evtToDisplay(displays[i])); } _pi.displays = newDisplays; stateGlobal.displaysCount.value = _pi.displays.length; @@ -1712,12 +1714,17 @@ class FFI { } } +const kInvalidResolutionValue = -1; +const kVirtualDisplayResolutionValue = 0; + class Display { double x = 0; double y = 0; int width = 0; int height = 0; bool cursorEmbedded = false; + int originalWidth = kInvalidResolutionValue; + int originalHeight = kInvalidResolutionValue; Display() { width = (isDesktop || isWebDesktop) @@ -1740,6 +1747,15 @@ class Display { other.width == width && other.height == height && other.cursorEmbedded == cursorEmbedded; + + bool get isOriginalResolutionSet => + originalWidth != kInvalidResolutionValue && + originalHeight != kInvalidResolutionValue; + bool get isVirtualDisplayResolution => + originalWidth == kVirtualDisplayResolutionValue && + originalHeight == kVirtualDisplayResolutionValue; + bool get isOriginalResolution => + width == originalWidth && height == originalHeight; } class Resolution { diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 0b0de28a1..1d8e76af4 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -41,6 +41,7 @@ message DisplayInfo { string name = 5; bool online = 6; bool cursor_embedded = 7; + Resolution original_resolution = 8; } message PortForward { @@ -444,6 +445,8 @@ message SwitchDisplay { int32 height = 5; bool cursor_embedded = 6; SupportedResolutions resolutions = 7; + // Do not care about the origin point for now. + Resolution original_resolution = 8; } message PermissionInfo { @@ -501,6 +504,7 @@ message OptionMessage { SupportedDecoding supported_decoding = 10; int32 custom_fps = 11; BoolOption disable_keyboard = 12; + Resolution custom_resolution = 13; } message TestDelay { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 224709911..fff6fc5a1 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -105,12 +105,9 @@ macro_rules! serde_field_string { where D: de::Deserializer<'de>, { - let s: &str = de::Deserialize::deserialize(deserializer).unwrap_or_default(); - Ok(if s.is_empty() { - Self::$default_func() - } else { - s.to_owned() - }) + let s: String = + de::Deserialize::deserialize(deserializer).unwrap_or(Self::$default_func()); + Ok(s) } }; } @@ -191,6 +188,12 @@ pub struct Config2 { pub options: HashMap, } +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct Resolution { + pub w: i32, + pub h: i32, +} + #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct PeerConfig { #[serde(default, deserialize_with = "deserialize_vec_u8")] @@ -246,6 +249,13 @@ pub struct PeerConfig { #[serde(flatten)] pub view_only: ViewOnly, + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_option_resolution" + )] + pub custom_resolution: Option, + // The other scalar value must before this #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] pub options: HashMap, // not use delete to represent default values @@ -1486,6 +1496,7 @@ deserialize_default!(deserialize_option_string, Option); deserialize_default!(deserialize_hashmap_string_string, HashMap); deserialize_default!(deserialize_hashmap_string_bool, HashMap); deserialize_default!(deserialize_hashmap_string_configoidcprovider, HashMap); +deserialize_default!(deserialize_option_resolution, Option); #[cfg(test)] mod tests { @@ -1534,4 +1545,34 @@ mod tests { }) ); } + + #[test] + fn test_peer_config_deserialize() { + let default_peer_config = toml::from_str::("").unwrap(); + // test custom_resolution + { + let wrong_type_str = r#" + view_style = "adaptive" + scroll_style = "scrollbar" + custom_resolution = true + "#; + let mut compare_config = default_peer_config.clone(); + compare_config.view_style = "adaptive".to_string(); + compare_config.scroll_style = "scrollbar".to_string(); + let cfg = toml::from_str::(wrong_type_str); + assert_eq!(cfg, Ok(compare_config), "Failed to test wrong_type_str"); + + let wrong_field_str = r#" + [custom_resolution] + w = 1920 + h = 1080 + hello = "world" + [ui_flutter] + "#; + let mut compare_config = default_peer_config.clone(); + compare_config.custom_resolution = Some(Resolution { w: 1920, h: 1080 }); + let cfg = toml::from_str::(wrong_field_str); + assert_eq!(cfg, Ok(compare_config), "Failed to test wrong_field_str"); + } + } } diff --git a/res/lang.py b/res/lang.py index 85fd08654..74492818a 100644 --- a/res/lang.py +++ b/res/lang.py @@ -36,7 +36,7 @@ def main(): def expand(): - for fn in glob.glob('./src/lang/*'): + for fn in glob.glob('./src/lang/*.rs'): lang = os.path.basename(fn)[:-3] if lang in ['en','template']: continue print(lang) diff --git a/src/client.rs b/src/client.rs index 8ec021f55..a43dbcca1 100644 --- a/src/client.rs +++ b/src/client.rs @@ -31,9 +31,11 @@ use hbb_common::{ allow_err, anyhow::{anyhow, Context}, bail, - config::{Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT}, + config::{ + Config, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, + }, get_version_number, log, - message_proto::{option_message::BoolOption, *}, + message_proto::{option_message::BoolOption, Resolution as ProtoResolution, *}, protobuf::Message as _, rand, rendezvous_proto::*, @@ -1400,6 +1402,16 @@ impl LoginConfigHandler { msg.disable_clipboard = BoolOption::Yes.into(); n += 1; } + if let Some(r) = self.get_custom_resolution() { + if r.0 > 0 && r.1 > 0 { + msg.custom_resolution = Some(ProtoResolution { + width: r.0, + height: r.1, + ..Default::default() + }) + .into(); + } + } msg.supported_decoding = hbb_common::protobuf::MessageField::some(Decoder::supported_decodings(Some(&self.id))); n += 1; @@ -1571,6 +1583,18 @@ impl LoginConfigHandler { } } + #[inline] + pub fn get_custom_resolution(&self) -> Option<(i32, i32)> { + self.config.custom_resolution.as_ref().map(|r| (r.w, r.h)) + } + + #[inline] + pub fn set_custom_resolution(&mut self, wh: Option<(i32, i32)>) { + let mut config = self.load_config(); + config.custom_resolution = wh.map(|r| Resolution { w: r.0, h: r.1 }); + self.save_config(config); + } + /// Get user name. /// Return the name of the given peer. If the peer has no name, return the name in the config. /// diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index d38e7f104..597c720ab 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1211,6 +1211,14 @@ impl Remote { s.cursor_embedded, ); } + let custom_resolution = if s.width != s.original_resolution.width + || s.height != s.original_resolution.height + { + Some((s.width, s.height)) + } else { + None + }; + self.handler.set_custom_resolution(custom_resolution); } Some(misc::Union::CloseReason(c)) => { self.handler.msgbox("error", "Connection Error", &c, ""); diff --git a/src/core_main.rs b/src/core_main.rs index 4aad72b90..92b56da82 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,10 +1,10 @@ #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::platform::breakdown_callback; +use hbb_common::log; #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::platform::register_breakdown_handler; -use hbb_common::{allow_err, log}; /// shared by flutter and sciter main function /// @@ -270,7 +270,7 @@ fn init_plugins(args: &Vec) { crate::plugin::init(); } } else if "--service" == (&args[0] as &str) { - allow_err!(crate::plugin::remove_uninstalled()); + hbb_common::allow_err!(crate::plugin::remove_uninstalled()); } } diff --git a/src/flutter.rs b/src/flutter.rs index f6461b744..1748d63df 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -286,6 +286,8 @@ impl FlutterHandler { h.insert("width", d.width); h.insert("height", d.height); h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + h.insert("original_width", d.original_resolution.width); + h.insert("original_height", d.original_resolution.height); msg_vec.push(h); } serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) @@ -618,6 +620,14 @@ impl InvokeUiSession for FlutterHandler { .to_string(), ), ("resolutions", &resolutions), + ( + "original_width", + &display.original_resolution.width.to_string(), + ), + ( + "original_height", + &display.original_resolution.height.to_string(), + ), ], ); } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9aadca1bc..d072c58a3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -880,6 +880,18 @@ pub fn main_handle_relay_id(id: String) -> String { handle_relay_id(id) } +pub fn main_get_current_display() -> SyncReturn { + 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(), + }; + SyncReturn(display_info) +} + pub fn session_add_port_forward( id: String, local_port: i32, @@ -1426,10 +1438,10 @@ pub fn plugin_event(_id: String, _peer: String, _event: Vec) { } } -pub fn plugin_register_event_stream(id: String, event2ui: StreamSink) { +pub fn plugin_register_event_stream(_id: String, _event2ui: StreamSink) { #[cfg(feature = "plugin_framework")] { - crate::plugin::native_handlers::session::session_register_event_stream(id, event2ui); + crate::plugin::native_handlers::session::session_register_event_stream(_id, _event2ui); } } @@ -1577,16 +1589,16 @@ pub fn plugin_list_reload() { } } -pub fn plugin_install(id: String, b: bool) { +pub fn plugin_install(_id: String, _b: bool) { #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { - if b { + if _b { if let Err(e) = crate::plugin::install_plugin(&id) { log::error!("Failed to install plugin '{}': {}", id, e); } } else { - crate::plugin::uninstall_plugin(&id, true); + crate::plugin::uninstall_plugin(&_id, true); } } } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0d876d6f2..5ca5cb6f1 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 447460174..b325dee22 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "启用"), ("Disable", "禁用"), ("Options", "选项"), + ("resolution_original_tip", "原始分辨率"), + ("resolution_fit_local_tip", "适应本地分辨率"), + ("resolution_custom_tip", "自定义分辨率"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e38e7620f..0596c3590 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index e863f534f..3db456bfe 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 50f8f3e7e..199ff886c 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Aktivieren"), ("Disable", "Deaktivieren"), ("Options", "Einstellungen"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 40c3d30d6..fafcf1698 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 93a56544a..8e86125c6 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -66,5 +66,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("xorg_not_found_text_tip", "Please install Xorg"), ("no_desktop_title_tip", "No desktop is available"), ("no_desktop_text_tip", "Please install GNOME desktop"), + ("resolution_original_tip", "Original resolution"), + ("resolution_fit_local_tip", "Fit local resolution"), + ("resolution_custom_tip", "Custom resolution"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 61e57d5f5..a18cc6ee0 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 6daf738ba..2b4b20115 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Habilitar"), ("Disable", "Inhabilitar"), ("Options", "Opciones"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index eb35b0ff1..b44e796fd 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "فعال کردن"), ("Disable", "غیر فعال کردن"), ("Options", "گزینه ها"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 09a84035e..dfcef4fc9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Activé"), ("Disable", "Desactivé"), ("Options", "Options"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3fca393b1..5828efb08 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index e746d509d..b90c96ec6 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index a0f2e9bab..e3c59bdae 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Abilita"), ("Disable", "Disabilita"), ("Options", "Opzioni"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index cf49377ec..4e8267aea 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index eab5f189f..32c34d086 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c0d90b433..e345c993c 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 953d0ad43..fe1336848 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 92f2c9557..bdcc4314c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Activeer"), ("Disable", "Deactiveer"), ("Options", "Opties"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 11bf58f19..9dffed6e9 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Włącz"), ("Disable", "Wyłącz"), ("Options", "Opcje"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 07bb8a065..e28f8b1cc 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 5627a5f2a..e3fa71ad9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Habilitar"), ("Disable", "Desabilitar"), ("Options", "Opções"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 04d9f9d22..81665e73a 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c2517df99..7d277450e 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", "Включить"), ("Disable", "Отключить"), ("Options", "Настройки"), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 0e2559193..cea678364 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index c6188aaf8..35edd0568 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index c55a4741e..c4e248ea2 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 3f37e23c7..5fc30af0b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 5a76a1677..78331d448 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f61635f9a..a20769abb 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index ab4ec4993..3ef4142c4 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 3fd57d1cd..7572cbee1 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b9cdb60dd..f624b2006 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 68580c73b..2f98d9fe2 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 9e64ca784..6781ca2d5 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -506,5 +506,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable", ""), ("Disable", ""), ("Options", ""), + ("resolution_original_tip", ""), + ("resolution_fit_local_tip", ""), + ("resolution_custom_tip", ""), ].iter().cloned().collect(); } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 147d42502..46f92d511 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -56,6 +56,10 @@ use windows_service::{ use winreg::enums::*; use winreg::RegKey; +// This string is defined here. +// https://github.com/fufesou/RustDeskIddDriver/blob/b370aad3f50028b039aad211df60c8051c4a64d6/RustDeskIddDriver/RustDeskIddDriver.inf#LL73C1-L73C40 +const IDD_DEVICE_STRING: &'static str = "RustDeskIddDriver Device\0"; + pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { #[allow(invalid_value)] @@ -1831,21 +1835,25 @@ pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { Ok(()) } +#[inline] +fn str_to_device_name(name: &str) -> [u16; 32] { + let mut device_name: Vec = wide_string(name); + if device_name.len() < 32 { + device_name.resize(32, 0); + } + let mut result = [0; 32]; + result.copy_from_slice(&device_name[..32]); + result +} + pub fn resolutions(name: &str) -> Vec { unsafe { let mut dm: DEVMODEW = std::mem::zeroed(); - let wname = wide_string(name); - let len = if wname.len() <= dm.dmDeviceName.len() { - wname.len() - } else { - dm.dmDeviceName.len() - }; - std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); - dm.dmSize = std::mem::size_of::() as _; let mut v = vec![]; let mut num = 0; + let device_name = str_to_device_name(name); loop { - if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 { + if EnumDisplaySettingsW(device_name.as_ptr(), num, &mut dm) == 0 { break; } let r = Resolution { @@ -1866,8 +1874,8 @@ pub fn current_resolution(name: &str) -> ResultType { unsafe { let mut dm: DEVMODEW = std::mem::zeroed(); dm.dmSize = std::mem::size_of::() as _; - let wname = wide_string(name); - if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { + let device_name = str_to_device_name(name); + if EnumDisplaySettingsW(device_name.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { bail!( "failed to get currrent resolution, errno={}", GetLastError() @@ -1882,25 +1890,57 @@ pub fn current_resolution(name: &str) -> ResultType { } } +#[inline] +fn is_device_name(device_name: &str, name: &str) -> bool { + if name.len() == device_name.len() { + name == device_name + } else if name.len() > device_name.len() { + false + } else { + &device_name[..name.len()] == name && device_name.as_bytes()[name.len() as usize] == 0 + } +} + +pub fn is_virtual_display(name: &str) -> ResultType { + let mut dd: DISPLAY_DEVICEW = unsafe { std::mem::zeroed() }; + dd.cb = std::mem::size_of::() as DWORD; + let mut i_dev_num = 0; + loop { + let result = unsafe { EnumDisplayDevicesW(null_mut(), i_dev_num, &mut dd, 0) }; + if result == 0 { + break; + } + if let Ok(device_name) = String::from_utf16(&dd.DeviceName) { + if is_device_name(&device_name, name) { + return match std::string::String::from_utf16(&dd.DeviceString) { + Ok(s) => Ok(&s[..IDD_DEVICE_STRING.len()] == IDD_DEVICE_STRING), + Err(e) => bail!("convert the device string of '{}' to string: {}", name, e), + }; + } + } + i_dev_num += 1; + } + bail!("No such display '{}'", name) +} + pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + let device_name = str_to_device_name(name); unsafe { let mut dm: DEVMODEW = std::mem::zeroed(); - if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) { + if FALSE == EnumDisplaySettingsW(device_name.as_ptr() as _, ENUM_CURRENT_SETTINGS, &mut dm) + { bail!("EnumDisplaySettingsW failed, errno={}", GetLastError()); } - let wname = wide_string(name); - let len = if wname.len() <= dm.dmDeviceName.len() { - wname.len() - } else { - dm.dmDeviceName.len() - }; - std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); - dm.dmSize = std::mem::size_of::() as _; + // dmPelsWidth and dmPelsHeight is the same to width and height + // Because this process is running in dpi awareness mode. + if dm.dmPelsWidth == width as u32 && dm.dmPelsHeight == height as u32 { + return Ok(()); + } dm.dmPelsWidth = width as _; dm.dmPelsHeight = height as _; dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; let res = ChangeDisplaySettingsExW( - wname.as_ptr(), + device_name.as_ptr(), &mut dm, NULL as _, CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, diff --git a/src/server.rs b/src/server.rs index 66095fd9e..2b833576a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -8,12 +8,10 @@ use std::{ use bytes::Bytes; pub use connection::*; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::config::Config2; use hbb_common::tcp::{self, new_listener}; use hbb_common::{ allow_err, - anyhow::{anyhow, Context}, + anyhow::Context, bail, config::{Config, CONNECT_TIMEOUT, RELAY_PORT}, log, @@ -25,6 +23,8 @@ use hbb_common::{ timeout, tokio, ResultType, Stream, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::{anyhow::anyhow, config::Config2}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{GenericService, Service, Subscriber}; diff --git a/src/server/connection.rs b/src/server/connection.rs index e537f744d..69cb56102 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -179,8 +179,6 @@ pub struct Connection { #[cfg(windows)] portable: PortableState, from_switch: bool, - #[cfg(not(any(target_os = "android", target_os = "ios")))] - origin_resolution: HashMap, voice_call_request_timestamp: Option, audio_input_device_before_voice_call: Option, options_in_login: Option, @@ -306,8 +304,6 @@ impl Connection { #[cfg(windows)] portable: Default::default(), from_switch: false, - #[cfg(not(any(target_os = "android", target_os = "ios")))] - origin_resolution: Default::default(), audio_sender: None, voice_call_request_timestamp: None, audio_input_device_before_voice_call: None, @@ -631,15 +627,18 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - conn.reset_resolution(); - ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); + let mut active_conns_lock = ALIVE_CONNS.lock().unwrap(); + active_conns_lock.retain(|&c| c != id); if let Some(s) = conn.server.upgrade() { let mut s = s.write().unwrap(); s.remove_connection(&conn.inner); #[cfg(not(any(target_os = "android", target_os = "ios")))] try_stop_record_cursor_pos(); } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if active_conns_lock.is_empty() { + video_service::reset_resolutions(); + } log::info!("#{} connection loop exited", id); } @@ -1044,6 +1043,8 @@ impl Connection { }) .into(); #[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_name() @@ -1893,25 +1894,7 @@ impl Connection { } } #[cfg(not(any(target_os = "android", target_os = "ios")))] - Some(misc::Union::ChangeResolution(r)) => { - if self.keyboard { - if let Ok(name) = video_service::get_current_display_name() { - if let Ok(current) = crate::platform::current_resolution(&name) { - if let Err(e) = crate::platform::change_resolution( - &name, - r.width as _, - r.height as _, - ) { - log::error!("change resolution failed:{:?}", e); - } else { - if !self.origin_resolution.contains_key(&name) { - self.origin_resolution.insert(name, current); - } - } - } - } - } - } + Some(misc::Union::ChangeResolution(r)) => self.change_resolution(&r), #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::PluginRequest(p)) => { @@ -1953,6 +1936,25 @@ impl Connection { true } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn change_resolution(&mut self, r: &Resolution) { + if self.keyboard { + if let Ok(name) = video_service::get_current_display_name() { + 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 + ); + } + } + } + } + pub async fn handle_voice_call(&mut self, accepted: bool) { if let Some(ts) = self.voice_call_request_timestamp.take() { let msg = new_voice_call_response(ts.get(), accepted); @@ -2147,6 +2149,12 @@ impl Connection { } } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(custom_resolution) = o.custom_resolution.as_ref() { + if custom_resolution.width > 0 && custom_resolution.height > 0 { + self.change_resolution(&custom_resolution); + } + } if self.keyboard { if let Ok(q) = o.block_input.enum_value() { match q { @@ -2262,20 +2270,6 @@ impl Connection { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn reset_resolution(&self) { - self.origin_resolution - .iter() - .map(|(name, r)| { - if let Err(e) = - crate::platform::change_resolution(&name, r.width as _, r.height as _) - { - log::error!("change resolution failed:{:?}", e); - } - }) - .count(); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] fn release_pressed_modifiers(&mut self) { for modifier in self.pressed_modifiers.iter() { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index bee115fbb..18b400fca 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -25,9 +25,12 @@ use crate::virtual_display_manager; use crate::{platform::windows::is_process_consent_running, privacy_win_mag}; #[cfg(windows)] use hbb_common::get_version_number; -use hbb_common::tokio::sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Mutex as TokioMutex, +use hbb_common::{ + protobuf::MessageField, + tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, + }, }; #[cfg(not(windows))] use scrap::Capturer; @@ -62,8 +65,70 @@ lazy_static::lazy_static! { 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 ORIGINAL_RESOLUTIONS: Arc>> = Default::default(); } +// Not virtual display +#[inline] +fn set_original_resolution_(display_name: &str, wh: (i32, i32)) -> (i32, i32) { + let mut original_resolutions = ORIGINAL_RESOLUTIONS.write().unwrap(); + match original_resolutions.get(display_name) { + Some(r) => r.clone(), + None => { + original_resolutions.insert(display_name.to_owned(), wh.clone()); + wh + } + } +} + +// Not virtual display +#[inline] +fn get_original_resolution_(display_name: &str) -> Option<(i32, i32)> { + ORIGINAL_RESOLUTIONS + .read() + .unwrap() + .get(display_name) + .map(|r| r.clone()) +} + +// Not virtual display +#[inline] +fn get_or_set_original_resolution_(display_name: &str, wh: (i32, i32)) -> (i32, i32) { + let r = get_original_resolution_(display_name); + if let Some(r) = r { + return r; + } + set_original_resolution_(display_name, wh) +} + +// Not virtual display +#[inline] +fn update_get_original_resolution_(display_name: &str, w: usize, h: usize) -> Resolution { + let wh = get_or_set_original_resolution_(display_name, (w as _, h as _)); + Resolution { + width: wh.0, + height: wh.1, + ..Default::default() + } +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn reset_resolutions() { + for (name, (w, h)) in ORIGINAL_RESOLUTIONS.read().unwrap().iter() { + 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(); @@ -71,22 +136,27 @@ fn is_capturer_mag_supported() -> bool { false } +#[inline] pub fn capture_cursor_embedded() -> bool { scrap::is_cursor_embedded() } +#[inline] pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() } +#[inline] pub fn set_privacy_mode_conn_id(conn_id: i32) { *PRIVACY_MODE_CONN_ID.lock().unwrap() = conn_id } +#[inline] 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 @@ -491,6 +561,8 @@ fn run(sp: GenericService) -> ResultType<()> { if *SWITCH.lock().unwrap() { log::debug!("Broadcasting display switch"); let mut misc = Misc::new(); + let display_name = get_current_display_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 _, @@ -500,12 +572,15 @@ fn run(sp: GenericService) -> ResultType<()> { cursor_embedded: capture_cursor_embedded(), #[cfg(not(any(target_os = "android", target_os = "ios")))] resolutions: Some(SupportedResolutions { - resolutions: get_current_display_name() - .map(|name| crate::platform::resolutions(&name)) - .unwrap_or(vec![]), + 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(); @@ -820,6 +895,38 @@ pub fn handle_one_frame_encoded( Ok(send_conn_ids) } +#[inline] +fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageField { + Some(if is_virtual_display(&display_name) { + Resolution { + width: 0, + height: 0, + ..Default::default() + } + } else { + update_get_original_resolution_(&display_name, w, h) + }) + .into() +} + +#[inline] +#[cfg(target_os = "windows")] +fn is_virtual_display(name: &str) -> bool { + match crate::platform::windows::is_virtual_display(&name) { + Ok(b) => b, + Err(e) => { + log::error!("Failed to check is virtual display for '{}': {}", &name, e); + false + } + } +} + +#[inline] +#[cfg(not(target_os = "windows"))] +fn is_virtual_display(_name: &str) -> bool { + false +} + pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { let mut displays = Vec::new(); let mut primary = 0; @@ -827,14 +934,17 @@ pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { 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: d.name(), + name: display_name, online: d.is_online(), cursor_embedded: false, + original_resolution, ..Default::default() }); } @@ -853,6 +963,15 @@ 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")] { @@ -860,10 +979,6 @@ pub async fn get_displays() -> ResultType<(usize, Vec)> { return super::wayland::get_displays().await; } } - // switch to primary display if long time (30 seconds) no users - if LAST_ACTIVE.lock().unwrap().elapsed().as_secs() >= 30 { - *CURRENT_DISPLAY.lock().unwrap() = usize::MAX; - } Ok(get_displays_2(&try_get_displays()?)) } @@ -957,6 +1072,8 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { get_current_display_2(try_get_displays()?) } +// `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. pub fn get_current_display_name() -> ResultType { Ok(get_current_display_2(try_get_displays()?)?.2.name()) } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a804484a0..8a43642d8 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -88,10 +88,7 @@ impl Session { } pub fn is_port_forward(&self) -> bool { - let conn_type = self.lc - .read() - .unwrap() - .conn_type; + let conn_type = self.lc.read().unwrap().conn_type; conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP } @@ -832,6 +829,11 @@ impl Session { } } + #[inline] + pub fn set_custom_resolution(&mut self, wh: Option<(i32, i32)>) { + self.lc.write().unwrap().set_custom_resolution(wh); + } + pub fn change_resolution(&self, width: i32, height: i32) { let mut misc = Misc::new(); misc.set_change_resolution(Resolution {