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 2575ddadd..da91e0dce 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -664,71 +664,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; - RxBool _isOrignalResolution = true.obs; - RxBool _isFitLocalResolution = false.obs; - - 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(); @@ -736,18 +689,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; @@ -758,7 +711,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) * @@ -793,7 +746,7 @@ class _DisplayMenuState extends State<_DisplayMenu> { } } - _updateScreen() async { + updateScreen() async { final v = await rustDeskWinManager.call( WindowType.Main, kWindowGetWindowInfo, ''); final String valueStr = v; @@ -813,8 +766,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; } @@ -833,7 +786,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 = @@ -843,6 +796,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( @@ -941,70 +965,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: [ - RdoMenuButton( - value: _kResolutionOrigin, - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - child: Text('Origin'), - ), - RdoMenuButton( - value: _kResolutionFitLocal, - groupValue: groupValue, - onChanged: onChanged, - ffi: widget.ffi, - child: Text('Fit local'), - ), - // RdoMenuButton( - // value: _kResolutionCustom, - // groupValue: groupValue, - // onChanged: onChanged, - // ffi: widget.ffi, - // child: Text('Custom resolution'), - // ), - ] + - 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), @@ -1023,6 +983,242 @@ 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; + late final _customWidth = + TextEditingController(text: display.width.toString()); + late final _customHeight = + TextEditingController(text: display.height.toString()); + + 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 visible = ffiModel.keyboard && resolutions.length > 1; + if (!visible) return Offstage(); + _groupValue = "${display.width}x${display.height}"; + + _getLocalResolution(); + return _SubmenuButton( + ffi: widget.ffi, + menuChildren: [ + _OriginalResolutionMenuButton(), + _FitLocalResolutionMenuButton(), + _customResolutionMenuButton(), + ] + + _supportedResolutionMenuButtons(), + child: Text(translate("Resolution"))); + } + + _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; + + int? w; + int? h; + if (value == _kResolutionOrigin) { + w = display.originalWidth; + h = display.originalHeight; + } else if (value == _kResolutionFitLocal) { + final resolution = _getBestFitResolution(); + if (resolution != null) { + w = resolution.width; + h = resolution.height; + } + } else if (value == _kResolutionCustom) { + debugPrint( + 'REMOVE ME ======================= ${_customWidth.value} ${_customHeight.value}'); + w = int.tryParse(_customWidth.value as String); + h = int.tryParse(_customHeight.value as String); + } 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) { + 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() { + return Offstage( + offstage: display.isOriginalResolution, + child: RdoMenuButton( + value: _kResolutionOrigin, + groupValue: _groupValue, + onChanged: _onChanged, + ffi: widget.ffi, + child: Text( + '${translate('Original')} ${display.originalWidth}x${display.originalHeight}'), + ), + ); + } + + Widget _FitLocalResolutionMenuButton() { + return Offstage( + offstage: _isRemoteResolutionFitLocal(), + child: RdoMenuButton( + value: _kResolutionFitLocal, + groupValue: _groupValue, + onChanged: _onChanged, + ffi: widget.ffi, + child: Text( + '${translate('Fit Local')} ${display.originalWidth}x${display.originalHeight}'), + ), + ); + } + + 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(); + + Widget _customResolutionMenuButton() { + return Offstage( + offstage: _isRemoteResolutionFitLocal(), + child: RdoMenuButton( + value: _kResolutionCustom, + groupValue: _groupValue, + onChanged: _onChanged, + ffi: widget.ffi, + child: _customResolutionWidget(), + ), + ); + } + + Widget _customResolutionWidget() { + return Column( + children: [ + Text(translate('Custom')), + SizedBox( + width: 5, + ), + _resolutionInput(_customWidth), + SizedBox( + width: 3, + ), + Text('x'), + SizedBox( + width: 3, + ), + _resolutionInput(_customHeight), + ], + ); + } + + TextField _resolutionInput(TextEditingController controller) { + return TextField( + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(4), + FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), + ], + controller: controller, + ); + } + + Resolution? _getBestFitResolution() { + if (_localResolution == null) { + return null; + } + + squareDistance(Resolution lhs, Resolution rhs) => + (lhs.width - rhs.width) * (lhs.width - rhs.width) + + (lhs.height - rhs.height) * (lhs.height - rhs.height); + + Resolution? res; + for (final r in resolutions) { + if (r.width <= _localResolution!.width && + r.height <= _localResolution!.height) { + if (res == null) { + res = r; + } else { + 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..a57a51752 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']) ?? newDisplay.originalWidth; + newDisplay.originalHeight = + int.tryParse(evt['original_height']) ?? newDisplay.originalHeight; _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'] ?? d.originalWidth; + d.originalHeight = evt['original_height'] ?? d.originalHeight; + 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; @@ -1718,6 +1720,8 @@ class Display { int width = 0; int height = 0; bool cursorEmbedded = false; + int originalWidth = 0; + int originalHeight = 0; Display() { width = (isDesktop || isWebDesktop) @@ -1740,6 +1744,9 @@ class Display { other.width == width && other.height == height && other.cursorEmbedded == cursorEmbedded; + + bool get isOriginalResolution => + width == originalWidth && height == originalHeight; } class Resolution { 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/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/ui_session_interface.rs b/src/ui_session_interface.rs index 3d5442ae0..8a43642d8 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -829,11 +829,12 @@ 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) { - self.lc - .write() - .unwrap() - .set_custom_resolution(Some((width, height))); let mut misc = Misc::new(); misc.set_change_resolution(Resolution { width,