diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index bba146fa8..1f4f74a33 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -995,10 +995,18 @@ class _ResolutionsMenu extends StatefulWidget { State<_ResolutionsMenu> createState() => _ResolutionsMenuState(); } +const double _kCustonResolutionEditingWidth = 42; +const _kCustomResolutionValue = 'custom'; + class _ResolutionsMenuState extends State<_ResolutionsMenu> { String _groupValue = ''; Resolution? _localResolution; + late final TextEditingController _customWidth = + TextEditingController(text: display.width.toString()); + late final TextEditingController _customHeight = + TextEditingController(text: display.height.toString()); + PeerInfo get pi => widget.ffi.ffiModel.pi; FfiModel get ffiModel => widget.ffi.ffiModel; Display get display => ffiModel.display; @@ -1012,22 +1020,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { @override Widget build(BuildContext context) { final isVirtualDisplay = display.isVirtualDisplayResolution; - // final visible = - // ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1); - final visible = ffiModel.keyboard && resolutions.length > 1; + final visible = + ffiModel.keyboard && (isVirtualDisplay || resolutions.length > 1); if (!visible) return Offstage(); - _groupValue = '${display.width}x${display.height}'; _getLocalResolution(); final showOriginalBtn = display.isOriginalResolutionSet && !display.isOriginalResolution; final showFitLocalBtn = !_isRemoteResolutionFitLocal(); - + _setGroupValue(); return _SubmenuButton( ffi: widget.ffi, menuChildren: [ _OriginalResolutionMenuButton(showOriginalBtn), _FitLocalResolutionMenuButton(showFitLocalBtn), - // _customResolutionMenuButton(isVirtualDisplay), + _customResolutionMenuButton(isVirtualDisplay), _menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay), ] + _supportedResolutionMenuButtons(), @@ -1035,11 +1041,20 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { ); } + _setGroupValue() { + final lastGroupValue = + stateGlobal.getLastResolutionGroupValue(widget.id, pi.currentDisplay); + if (lastGroupValue == _kCustomResolutionValue) { + _groupValue = _kCustomResolutionValue; + } else { + _groupValue = '${display.width}x${display.height}'; + } + } + _menuDivider( bool showOriginalBtn, bool showFitLocalBtn, bool isVirtualDisplay) { return Offstage( - // offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay), - offstage: !(showOriginalBtn || showFitLocalBtn), + offstage: !(showOriginalBtn || showFitLocalBtn || isVirtualDisplay), child: Divider(), ); } @@ -1060,12 +1075,25 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { } _onChanged(String? value) async { + stateGlobal.setLastResolutionGroupValue( + widget.id, pi.currentDisplay, value); 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) { + + int? w; + int? h; + if (value == _kCustomResolutionValue) { + w = int.tryParse(_customWidth.text); + h = int.tryParse(_customHeight.text); + } else { + final list = value.split('x'); + if (list.length == 2) { + w = int.tryParse(list[0]); + h = int.tryParse(list[1]); + } + } + + if (w != null && h != null) { + if (w != display.width || h != display.height) { await _changeResolution(w, h); } } @@ -1117,6 +1145,49 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { ); } + Widget _customResolutionMenuButton(isVirtualDisplay) { + return Offstage( + offstage: !isVirtualDisplay, + child: RdoMenuButton( + value: _kCustomResolutionValue, + groupValue: _groupValue, + onChanged: _onChanged, + ffi: widget.ffi, + child: Row( + children: [ + Text('${translate('resolution_custom_tip')} '), + SizedBox( + width: _kCustonResolutionEditingWidth, + child: _resolutionInput(_customWidth), + ), + Text(' x '), + SizedBox( + width: _kCustonResolutionEditingWidth, + child: _resolutionInput(_customHeight), + ), + ], + ), + ), + ); + } + + TextField _resolutionInput(TextEditingController controller) { + return TextField( + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.fromLTRB(3, 3, 3, 3), + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), + ], + controller: controller, + ); + } + List _supportedResolutionMenuButtons() => resolutions .map((e) => RdoMenuButton( value: '${e.width}x${e.height}', @@ -1655,14 +1726,14 @@ class RdoMenuButton extends StatelessWidget { final ValueChanged? onChanged; final Widget? child; final FFI ffi; - const RdoMenuButton( - {Key? key, - required this.value, - required this.groupValue, - required this.onChanged, - required this.child, - required this.ffi}) - : super(key: key); + const RdoMenuButton({ + Key? key, + required this.value, + required this.groupValue, + required this.child, + required this.ffi, + this.onChanged, + }) : super(key: key); @override Widget build(BuildContext context) { diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 294b348b9..a99b0ddd7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -503,6 +503,9 @@ class FfiModel with ChangeNotifier { } } } + + stateGlobal.resetLastResolutionGroupValues(peerId); + notifyListeners(); } diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 187b1ffc5..72b78dbc2 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -12,12 +12,14 @@ class StateGlobal { bool _maximize = false; bool grabKeyboard = false; final RxBool _showTabBar = true.obs; - final RxBool _showResizeEdge = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; final RxInt displaysCount = 0.obs; + // Use for desktop -> remote toolbar -> resolution + final Map> _lastResolutionGroupValues = {}; + int get windowId => _windowId; bool get fullscreen => _fullscreen; bool get maximize => _maximize; @@ -26,6 +28,22 @@ class StateGlobal { RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get windowBorderWidth => _windowBorderWidth; + resetLastResolutionGroupValues(String peerId) { + _lastResolutionGroupValues[peerId] = {}; + } + + setLastResolutionGroupValue( + String peerId, int currentDisplay, String? value) { + if (!_lastResolutionGroupValues.containsKey(peerId)) { + _lastResolutionGroupValues[peerId] = {}; + } + _lastResolutionGroupValues[peerId]![currentDisplay] = value; + } + + String? getLastResolutionGroupValue(String peerId, int currentDisplay) { + return _lastResolutionGroupValues[peerId]?[currentDisplay]; + } + setWindowId(int id) => _windowId = id; setMaximize(bool v) { if (_maximize != v && !_fullscreen) { @@ -33,12 +51,12 @@ class StateGlobal { _resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize; } } + setFullscreen(bool v) { if (_fullscreen != v) { _fullscreen = v; _showTabBar.value = !_fullscreen; - _resizeEdgeSize.value = - fullscreen + _resizeEdgeSize.value = fullscreen ? kFullScreenEdgeSize : _maximize ? kMaximizeEdgeSize diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 46f92d511..e2bb2ca83 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -56,10 +56,6 @@ 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)] @@ -1890,38 +1886,7 @@ 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); diff --git a/src/server/connection.rs b/src/server/connection.rs index 69cb56102..9bffebf4f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1940,6 +1940,16 @@ impl Connection { fn change_resolution(&mut self, r: &Resolution) { if self.keyboard { if let Ok(name) = video_service::get_current_display_name() { + #[cfg(target_os = "windows")] + if let Some(_ok) = + crate::virtual_display_manager::change_resolution_if_is_virtual_display( + &name, + r.width as _, + r.height as _, + ) + { + return; + } if let Err(e) = crate::platform::change_resolution(&name, r.width as _, r.height as _) { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 18b400fca..6bb491758 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -897,7 +897,11 @@ pub fn handle_one_frame_encoded( #[inline] fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageField { - Some(if is_virtual_display(&display_name) { + #[cfg(target_os = "windows")] + let is_virtual_display = crate::virtual_display_manager::is_virtual_display(&display_name); + #[cfg(not(target_os = "windows"))] + let is_virtual_display = false; + Some(if is_virtual_display { Resolution { width: 0, height: 0, @@ -909,24 +913,6 @@ fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageFie .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; diff --git a/src/virtual_display_manager.rs b/src/virtual_display_manager.rs index 08cc0d485..be6640309 100644 --- a/src/virtual_display_manager.rs +++ b/src/virtual_display_manager.rs @@ -1,6 +1,6 @@ use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, sync::{Arc, Mutex}, }; @@ -16,8 +16,8 @@ lazy_static::lazy_static! { #[derive(Default)] struct VirtualDisplayManager { - headless_index: Option, - peer_required_indices: HashSet, + headless_index_name: Option<(u32, String)>, + peer_index_name: HashMap, } impl VirtualDisplayManager { @@ -54,14 +54,16 @@ pub fn plug_in_headless() -> ResultType<()> { height: 1080, sync: 60, }]; + let device_names = windows::get_device_names(); VirtualDisplayManager::plug_in_monitor(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, &modes)?; - manager.headless_index = Some(VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS); + let device_name = get_new_device_name(&device_names); + manager.headless_index_name = Some((VIRTUAL_DISPLAY_INDEX_FOR_HEADLESS, device_name)); Ok(()) } pub fn plug_out_headless() -> bool { let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); - if let Some(index) = manager.headless_index.take() { + if let Some((index, _)) = manager.headless_index_name.take() { if let Err(e) = virtual_display::plug_out_monitor(index) { log::error!("Plug out monitor failed {}", e); } @@ -71,19 +73,39 @@ pub fn plug_out_headless() -> bool { } } -pub fn plug_in_peer_required( - modes: Vec>, -) -> ResultType> { +fn get_new_device_name(device_names: &HashSet) -> String { + for _ in 0..3 { + let device_names_af = windows::get_device_names(); + let diff_names: Vec<_> = device_names_af.difference(&device_names).collect(); + if diff_names.len() == 1 { + return diff_names[0].clone(); + } else if diff_names.len() > 1 { + log::error!( + "Failed to get diff device names after plugin virtual display, more than one diff names: {:?}", + &diff_names + ); + return "".to_string(); + } + // Sleep is needed here to wait for the virtual display to be ready. + std::thread::sleep(std::time::Duration::from_millis(50)); + } + log::error!("Failed to get diff device names after plugin virtual display",); + "".to_string() +} + +pub fn plug_in_peer_request(modes: Vec>) -> ResultType> { let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); VirtualDisplayManager::prepare_driver()?; let mut indices: Vec = Vec::new(); for m in modes.iter() { for idx in VIRTUAL_DISPLAY_START_FOR_PEER..VIRTUAL_DISPLAY_MAX_COUNT { - if !manager.peer_required_indices.contains(&idx) { + if !manager.peer_index_name.contains_key(&idx) { + let device_names = windows::get_device_names(); match VirtualDisplayManager::plug_in_monitor(idx, m) { Ok(_) => { - manager.peer_required_indices.insert(idx); + let device_name = get_new_device_name(&device_names); + manager.peer_index_name.insert(idx, device_name); indices.push(idx); } Err(e) => { @@ -97,13 +119,135 @@ pub fn plug_in_peer_required( Ok(indices) } -pub fn plug_out_peer_required(modes: &[u32]) -> ResultType<()> { +pub fn plug_out_peer_request(modes: &[u32]) -> ResultType<()> { let mut manager = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); for idx in modes.iter() { - if manager.peer_required_indices.contains(idx) { + if manager.peer_index_name.contains_key(idx) { allow_err!(virtual_display::plug_out_monitor(*idx)); - manager.peer_required_indices.remove(idx); + manager.peer_index_name.remove(idx); } } Ok(()) } + +pub fn is_virtual_display(name: &str) -> bool { + let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + if let Some((_, device_name)) = &lock.headless_index_name { + if windows::is_device_name(device_name, name) { + return true; + } + } + for (k, v) in lock.peer_index_name.iter() { + if windows::is_device_name(v, name) { + return true; + } + } + false +} + +fn change_resolution(index: u32, w: u32, h: u32) -> bool { + let modes = [virtual_display::MonitorMode { + width: w, + height: h, + sync: 60, + }]; + match virtual_display::update_monitor_modes(index, &modes) { + Ok(_) => true, + Err(e) => { + log::error!("Update monitor {} modes {:?} failed: {}", index, &modes, e); + false + } + } +} + +pub fn change_resolution_if_is_virtual_display(name: &str, w: u32, h: u32) -> Option { + let lock = VIRTUAL_DISPLAY_MANAGER.lock().unwrap(); + if let Some((index, device_name)) = &lock.headless_index_name { + if windows::is_device_name(device_name, name) { + return Some(change_resolution(*index, w, h)); + } + } + + for (k, v) in lock.peer_index_name.iter() { + if windows::is_device_name(v, name) { + return Some(change_resolution(*k, w, h)); + } + } + None +} + +mod windows { + use std::{collections::HashSet, ptr::null_mut}; + use winapi::{ + shared::minwindef::{DWORD, FALSE}, + um::{ + wingdi::{ + DEVMODEW, DISPLAY_DEVICEW, DISPLAY_DEVICE_ACTIVE, DISPLAY_DEVICE_MIRRORING_DRIVER, + }, + winuser::{EnumDisplayDevicesW, EnumDisplaySettingsExW, ENUM_CURRENT_SETTINGS}, + }, + }; + + // 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"; + + #[inline] + pub(super) 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(super) fn get_device_names() -> HashSet { + let mut device_names = HashSet::new(); + 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; + } + i_dev_num += 1; + + if 0 == (dd.StateFlags & DISPLAY_DEVICE_ACTIVE) + || (dd.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) > 0 + { + continue; + } + + let mut dm: DEVMODEW = unsafe { std::mem::zeroed() }; + dm.dmSize = std::mem::size_of::() as _; + dm.dmDriverExtra = 0; + let ok = unsafe { + EnumDisplaySettingsExW( + dd.DeviceName.as_ptr(), + ENUM_CURRENT_SETTINGS, + &mut dm as _, + 0, + ) + }; + if ok == FALSE { + continue; + } + if dm.dmPelsHeight == 0 || dm.dmPelsWidth == 0 { + continue; + } + + if let (Ok(device_name), Ok(device_string)) = ( + String::from_utf16(&dd.DeviceName), + String::from_utf16(&dd.DeviceString), + ) { + if &device_string[..IDD_DEVICE_STRING.len()] == IDD_DEVICE_STRING { + device_names.insert(device_name); + } + } + } + device_names + } +}