diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 1f4f74a33..d8871464d 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -995,7 +995,7 @@ class _ResolutionsMenu extends StatefulWidget { State<_ResolutionsMenu> createState() => _ResolutionsMenuState(); } -const double _kCustonResolutionEditingWidth = 42; +const double _kCustomResolutionEditingWidth = 42; const _kCustomResolutionValue = 'custom'; class _ResolutionsMenuState extends State<_ResolutionsMenu> { @@ -1102,6 +1102,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { _changeResolution(int w, int h) async { await bind.sessionChangeResolution( id: widget.id, + display: pi.currentDisplay, width: w, height: h, ); @@ -1157,12 +1158,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { children: [ Text('${translate('resolution_custom_tip')} '), SizedBox( - width: _kCustonResolutionEditingWidth, + width: _kCustomResolutionEditingWidth, child: _resolutionInput(_customWidth), ), Text(' x '), SizedBox( - width: _kCustonResolutionEditingWidth, + width: _kCustomResolutionEditingWidth, child: _resolutionInput(_customHeight), ), ], diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 29cbb35c2..d89370cc7 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -508,7 +508,8 @@ message OptionMessage { SupportedDecoding supported_decoding = 10; int32 custom_fps = 11; BoolOption disable_keyboard = 12; - Resolution custom_resolution = 13; +// Position 13 is used for Resolution. Remove later. +// Resolution custom_resolution = 13; } message TestDelay { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 8e0de8c0b..d668067c7 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -262,10 +262,10 @@ pub struct PeerConfig { #[serde( default, - skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_option_resolution" + deserialize_with = "deserialize_hashmap_resolutions", + skip_serializing_if = "HashMap::is_empty" )] - pub custom_resolution: Option, + pub custom_resolutions: HashMap, // The other scalar value must before this #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] @@ -1509,7 +1509,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); +deserialize_default!(deserialize_hashmap_resolutions, HashMap); #[cfg(test)] mod tests { @@ -1567,7 +1567,20 @@ mod tests { let wrong_type_str = r#" view_style = "adaptive" scroll_style = "scrollbar" - custom_resolution = true + custom_resolutions = true + "#; + let mut cfg_to_compare = default_peer_config.clone(); + cfg_to_compare.view_style = "adaptive".to_string(); + cfg_to_compare.scroll_style = "scrollbar".to_string(); + let cfg = toml::from_str::(wrong_type_str); + assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); + + let wrong_type_str = r#" + view_style = "adaptive" + scroll_style = "scrollbar" + [custom_resolutions.0] + w = "1920" + h = 1080 "#; let mut cfg_to_compare = default_peer_config.clone(); cfg_to_compare.view_style = "adaptive".to_string(); @@ -1576,14 +1589,15 @@ mod tests { assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); let wrong_field_str = r#" - [custom_resolution] + [custom_resolutions.0] w = 1920 h = 1080 hello = "world" [ui_flutter] "#; let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.custom_resolution = Some(Resolution { w: 1920, h: 1080 }); + cfg_to_compare.custom_resolutions = + HashMap::from([("0".to_string(), Resolution { w: 1920, h: 1080 })]); let cfg = toml::from_str::(wrong_field_str); assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str"); } diff --git a/src/client.rs b/src/client.rs index a43dbcca1..b64e31ec7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -35,7 +35,7 @@ use hbb_common::{ Config, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, }, get_version_number, log, - message_proto::{option_message::BoolOption, Resolution as ProtoResolution, *}, + message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, rendezvous_proto::*, @@ -1353,7 +1353,9 @@ impl LoginConfigHandler { /// /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { - if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) || self.conn_type.eq(&ConnType::RDP) + if self.conn_type.eq(&ConnType::FILE_TRANSFER) + || self.conn_type.eq(&ConnType::PORT_FORWARD) + || self.conn_type.eq(&ConnType::RDP) { return None; } @@ -1402,16 +1404,6 @@ 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; @@ -1424,7 +1416,9 @@ impl LoginConfigHandler { } pub fn get_option_message_after_login(&self) -> Option { - if self.conn_type.eq(&ConnType::FILE_TRANSFER) || self.conn_type.eq(&ConnType::PORT_FORWARD) || self.conn_type.eq(&ConnType::RDP) + if self.conn_type.eq(&ConnType::FILE_TRANSFER) + || self.conn_type.eq(&ConnType::PORT_FORWARD) + || self.conn_type.eq(&ConnType::RDP) { return None; } @@ -1584,14 +1578,27 @@ impl LoginConfigHandler { } #[inline] - pub fn get_custom_resolution(&self) -> Option<(i32, i32)> { - self.config.custom_resolution.as_ref().map(|r| (r.w, r.h)) + pub fn get_custom_resolution(&self, display: i32) -> Option<(i32, i32)> { + self.config + .custom_resolutions + .get(&display.to_string()) + .map(|r| (r.w, r.h)) } #[inline] - pub fn set_custom_resolution(&mut self, wh: Option<(i32, i32)>) { + pub fn set_custom_resolution(&mut self, display: i32, wh: Option<(i32, i32)>) { + let display = display.to_string(); let mut config = self.load_config(); - config.custom_resolution = wh.map(|r| Resolution { w: r.0, h: r.1 }); + match wh { + Some((w, h)) => { + config + .custom_resolutions + .insert(display, Resolution { w, h }); + } + None => { + config.custom_resolutions.remove(&display); + } + } self.save_config(config); } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index f7f022b6e..906d06ec7 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1201,7 +1201,7 @@ impl Remote { } } Some(misc::Union::SwitchDisplay(s)) => { - self.handler.ui_handler.switch_display(&s); + self.handler.handle_peer_switch_display(&s); self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { self.handler.set_display( @@ -1212,14 +1212,6 @@ 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/flutter_ffi.rs b/src/flutter_ffi.rs index bf56c8d29..0e3aa59c3 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -535,9 +535,9 @@ pub fn session_switch_sides(id: String) { } } -pub fn session_change_resolution(id: String, width: i32, height: i32) { +pub fn session_change_resolution(id: String, display: i32, width: i32, height: i32) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.change_resolution(width, height); + session.change_resolution(display, width, height); } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 8c211df39..a5ef3f20f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1793,6 +1793,14 @@ 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() + }); + } } Some(misc::Union::ChatMessage(c)) => { self.send_to_cm(ipc::Data::ChatMessage { text: c.text }); @@ -2159,12 +2167,6 @@ 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 { diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 22ad83001..def926da5 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -1029,12 +1029,14 @@ pub(super) fn get_current_display_2(mut all: Vec) -> ResultType<(usize, return Ok((n, current, all.remove(current))); } +#[inline] 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. +#[inline] 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 f6b19e8ea..d754d49d4 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -15,13 +15,21 @@ use bytes::Bytes; use rdev::{Event, EventType::*, KeyCode}; use uuid::Uuid; -use hbb_common::config::{Config, LocalConfig, PeerConfig}; #[cfg(not(feature = "flutter"))] use hbb_common::fs; -use hbb_common::rendezvous_proto::ConnType; -use hbb_common::tokio::{self, sync::mpsc}; -use hbb_common::{allow_err, message_proto::*}; -use hbb_common::{get_version_number, log, Stream}; +use hbb_common::{ + allow_err, + config::{Config, LocalConfig, PeerConfig}, + get_version_number, log, + message_proto::*, + rendezvous_proto::ConnType, + tokio::{ + self, + sync::mpsc, + time::{Duration as TokioDuration, Instant}, + }, + Stream, +}; use crate::client::io_loop::Remote; use crate::client::{ @@ -37,6 +45,8 @@ use crate::{client::Data, client::Interface}; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub static IS_IN: AtomicBool = AtomicBool::new(false); +const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15; + #[derive(Clone, Default)] pub struct Session { pub id: String, @@ -49,6 +59,7 @@ pub struct Session { pub server_keyboard_enabled: Arc>, pub server_file_transfer_enabled: Arc>, pub server_clipboard_enabled: Arc>, + pub last_change_display: Arc>, } #[derive(Clone)] @@ -59,6 +70,43 @@ pub struct SessionPermissionConfig { pub server_clipboard_enabled: Arc>, } +pub struct ChangeDisplayRecord { + time: Instant, + display: i32, + width: i32, + height: i32, +} + +impl Default for ChangeDisplayRecord { + fn default() -> Self { + Self { + time: Instant::now() + - TokioDuration::from_secs(CHANGE_RESOLUTION_VALID_TIMEOUT_SECS + 1), + display: 0, + width: 0, + height: 0, + } + } +} + +impl ChangeDisplayRecord { + fn new(display: i32, width: i32, height: i32) -> Self { + Self { + time: Instant::now(), + display, + width, + height, + } + } + + pub fn is_the_same_record(&self, display: i32, width: i32, height: i32) -> bool { + self.time.elapsed().as_secs() < CHANGE_RESOLUTION_VALID_TIMEOUT_SECS + && self.display == display + && self.width == width + && self.height == height + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] impl SessionPermissionConfig { pub fn is_text_clipboard_required(&self) -> bool { @@ -485,9 +533,16 @@ impl Session { } pub fn switch_display(&self, display: i32) { + let (w, h) = match self.lc.read().unwrap().get_custom_resolution(display) { + Some((w, h)) => (w, h), + None => (0, 0), + }; + let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { display, + width: w, + height: h, ..Default::default() }); let mut msg_out = Message::new(); @@ -831,12 +886,41 @@ impl Session { } } - #[inline] - pub fn set_custom_resolution(&mut self, wh: Option<(i32, i32)>) { - self.lc.write().unwrap().set_custom_resolution(wh); + pub fn handle_peer_switch_display(&self, display: &SwitchDisplay) { + self.ui_handler.switch_display(display); + + if self.last_change_display.lock().unwrap().is_the_same_record( + display.display, + display.width, + display.height, + ) { + let custom_resolution = if display.width != display.original_resolution.width + || display.height != display.original_resolution.height + { + Some((display.width, display.height)) + } else { + None + }; + self.lc + .write() + .unwrap() + .set_custom_resolution(display.display, custom_resolution); + } } - pub fn change_resolution(&self, width: i32, height: i32) { + pub fn change_resolution(&self, display: i32, width: i32, height: i32) { + *self.last_change_display.lock().unwrap() = + ChangeDisplayRecord::new(display, width, height); + self.do_change_resolution(display, width, height); + } + + fn try_change_init_resolution(&self, display: i32) { + if let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) { + self.do_change_resolution(display, w, h); + } + } + + fn do_change_resolution(&self, display: i32, width: i32, height: i32) { let mut misc = Misc::new(); misc.set_change_resolution(Resolution { width, @@ -1006,6 +1090,7 @@ impl Interface for Session { self.msgbox("error", "Remote Error", "No Display", ""); return; } + self.try_change_init_resolution(pi.current_display); let p = self.lc.read().unwrap().should_auto_login(); if !p.is_empty() { input_os_password(p, true, self.clone());