diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 7fcc7b3a7..24a9b3fad 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -138,6 +138,11 @@ const kRemoteScrollStyleAuto = 'scrollauto'; /// [kRemoteScrollStyleBar] Scroll image with scroll bar. const kRemoteScrollStyleBar = 'scrollbar'; +/// [kScrollModeDefault] Mouse or touchpad, the default scroll mode. +const kScrollModeDefault = 'default'; +/// [kScrollModeReverse] Mouse or touchpad, the reverse scroll mode. +const kScrollModeReverse = 'reverse'; + /// [kRemoteImageQualityBest] Best image quality. const kRemoteImageQualityBest = 'best'; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 3a33c7e5b..38b2d3c57 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -105,6 +105,7 @@ class _DesktopSettingPageState extends State _TabInfo('Network', Icons.link_outlined, Icons.link), _TabInfo( 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows), + _TabInfo('Input', Icons.keyboard_outlined, Icons.keyboard), _TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('About', Icons.info_outline, Icons.info) ]; @@ -121,6 +122,7 @@ class _DesktopSettingPageState extends State _Safety(), _Network(), _Display(), + _Input(), _Account(), _About(), ]; @@ -1220,6 +1222,50 @@ class _DisplayState extends State<_Display> { } } +class _Input extends StatefulWidget { + const _Input({Key? key}) : super(key: key); + + @override + State<_Input> createState() => _InputState(); +} + +class _InputState extends State<_Input> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + controller: scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + children: [ + scrollMode(context), + ]).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget scrollMode(BuildContext context) { + final key = 'scroll_mode'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default Scroll Mode', children: [ + _Radio(context, + value: kScrollModeDefault, + groupValue: groupValue, + label: 'Default scroll', + onChanged: onChanged), + _Radio(context, + value: kScrollModeReverse, + groupValue: groupValue, + label: 'Reverse scroll', + onChanged: onChanged), + ]); + } +} + class _Account extends StatefulWidget { const _Account({Key? key}) : super(key: key); diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index b59ae3736..a6aa1fd01 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -101,7 +101,7 @@ class ToolbarState { class _ToolbarTheme { static const Color blueColor = MyTheme.button; static const Color hoverBlueColor = MyTheme.accent; - static Color inactiveColor = Colors.grey[800]!; + static Color inactiveColor = Colors.grey[800]!; static Color hoverInactiveColor = Colors.grey[850]!; static const Color redColor = Colors.redAccent; @@ -546,9 +546,11 @@ class _PinMenu extends StatelessWidget { assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg", tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar', onPressed: state.switchPin, - color: state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor, - hoverColor: - state.pin ? _ToolbarTheme.hoverBlueColor : _ToolbarTheme.hoverInactiveColor, + color: + state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor, + hoverColor: state.pin + ? _ToolbarTheme.hoverBlueColor + : _ToolbarTheme.hoverInactiveColor, ), ); } @@ -561,15 +563,18 @@ class _MobileActionMenu extends StatelessWidget { @override Widget build(BuildContext context) { if (!ffi.ffiModel.isPeerAndroid) return Offstage(); - return Obx(()=>_IconMenuButton( - assetName: 'assets/actions_mobile.svg', - tooltip: 'Mobile Actions', - onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), - color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue - ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor, - hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue - ? _ToolbarTheme.hoverBlueColor : _ToolbarTheme.hoverInactiveColor, - )); + return Obx(() => _IconMenuButton( + assetName: 'assets/actions_mobile.svg', + tooltip: 'Mobile Actions', + onPressed: () => + ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), + color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue + ? _ToolbarTheme.blueColor + : _ToolbarTheme.inactiveColor, + hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue + ? _ToolbarTheme.hoverBlueColor + : _ToolbarTheme.hoverInactiveColor, + )); } } @@ -1304,23 +1309,25 @@ class _KeyboardMenu extends StatelessWidget { color: _ToolbarTheme.blueColor, hoverColor: _ToolbarTheme.hoverBlueColor, menuChildren: [ - mode(modeOnly), + keyboardMode(modeOnly), localKeyboardType(), Divider(), - view_mode(), + viewMode(), + Divider(), + scrollMode(), ]); } - mode(String? modeOnly) { + keyboardMode(String? modeOnly) { return futureBuilder(future: () async { return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ?? _kKeyLegacyMode; }(), hasData: (data) { final groupValue = data as String; - List modes = [ - KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), - KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), - KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), + List modes = [ + InputModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), + InputModeMenu(key: _kKeyMapMode, menu: 'Map mode'), + InputModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), ]; List list = []; final enabled = !ffi.ffiModel.viewOnly; @@ -1330,7 +1337,7 @@ class _KeyboardMenu extends StatelessWidget { sessionId: ffi.sessionId, value: value); } - for (KeyboardModeMenu mode in modes) { + for (InputModeMenu mode in modes) { if (modeOnly != null && mode.key != modeOnly) { continue; } else if (!bind.sessionIsKeyboardModeSupported( @@ -1379,7 +1386,7 @@ class _KeyboardMenu extends StatelessWidget { ); } - view_mode() { + viewMode() { final ffiModel = ffi.ffiModel; final enabled = version_cmp(pi.version, '1.2.0') >= 0 && ffiModel.keyboard; return CkbMenuButton( @@ -1395,6 +1402,40 @@ class _KeyboardMenu extends StatelessWidget { ffi: ffi, child: Text(translate('View Mode'))); } + + scrollMode() { + return futureBuilder(future: () async { + final mode = await bind.sessionGetScrollMode(sessionId: ffi.sessionId); + if (mode != null) { + return mode; + } + return bind.mainGetUserDefaultOption(key: 'scroll_mode'); + }(), hasData: (data) { + final groupValue = data as String; + List modes = [ + InputModeMenu(key: kScrollModeDefault, menu: 'Default mode'), + InputModeMenu(key: kScrollModeReverse, menu: 'Reverse mode'), + ]; + List list = []; + final enabled = !ffi.ffiModel.viewOnly; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetScrollMode(sessionId: ffi.sessionId, value: value); + } + + for (InputModeMenu mode in modes) { + var text = translate(mode.menu); + list.add(RdoMenuButton( + child: Text(text), + value: mode.key, + groupValue: groupValue, + onChanged: enabled ? onChanged : null, + ffi: ffi, + )); + } + return Column(children: list); + }); + } } class _ChatMenu extends StatefulWidget { @@ -1592,26 +1633,26 @@ class _IconMenuButtonState extends State<_IconMenuButton> { width: _ToolbarTheme.buttonSize, height: _ToolbarTheme.buttonSize, child: MenuItemButton( - style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll(Colors.transparent), - padding: MaterialStatePropertyAll(EdgeInsets.zero), - overlayColor: MaterialStatePropertyAll(Colors.transparent)), - onHover: (value) => setState(() { - hover = value; - }), - onPressed: widget.onPressed, - child: Tooltip( - message: translate(widget.tooltip), - child: Material( - type: MaterialType.transparency, - child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(_ToolbarTheme.iconRadius), - color: hover ? widget.hoverColor : widget.color, - ), - child: icon)), - ) - ), + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.transparent), + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + onPressed: widget.onPressed, + child: Tooltip( + message: translate(widget.tooltip), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_ToolbarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon)), + )), ).marginSymmetric( horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin, vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin); @@ -1675,18 +1716,17 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> { onHover: (value) => setState(() { hover = value; }), - child: Tooltip( - message: translate(widget.tooltip), - child: Material( + child: Tooltip( + message: translate(widget.tooltip), + child: Material( type: MaterialType.transparency, child: Ink( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(_ToolbarTheme.iconRadius), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_ToolbarTheme.iconRadius), color: hover ? widget.hoverColor : widget.color, - ), - child: icon)) - ), + ), + child: icon))), menuChildren: widget.menuChildren .map((e) => _buildPointerTrackWidget(e, widget.ffi)) .toList())); @@ -1973,11 +2013,11 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { } } -class KeyboardModeMenu { +class InputModeMenu { final String key; final String menu; - KeyboardModeMenu({required this.key, required this.menu}); + InputModeMenu({required this.key, required this.menu}); } _menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index a48da5ff0..4d89d06cd 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -230,6 +230,7 @@ pub struct PeerConfig { skip_serializing_if = "String::is_empty" )] pub view_style: String, + // Image scroll style, scrollbar or scroll auto #[serde( default = "PeerConfig::default_scroll_style", deserialize_with = "PeerConfig::deserialize_scroll_style", @@ -276,6 +277,13 @@ pub struct PeerConfig { pub keyboard_mode: String, #[serde(flatten)] pub view_only: ViewOnly, + // Mouse wheel or touchpad scroll mode, default or reverse + #[serde( + default = "PeerConfig::default_scroll_mode", + deserialize_with = "PeerConfig::deserialize_scroll_mode", + skip_serializing_if = "String::is_empty" + )] + pub scroll_mode: String, #[serde( default, @@ -319,6 +327,7 @@ impl Default for PeerConfig { show_quality_monitor: Default::default(), keyboard_mode: Default::default(), view_only: Default::default(), + scroll_mode: Self::default_scroll_mode(), custom_resolutions: Default::default(), options: Self::default_options(), ui_flutter: Default::default(), @@ -1130,6 +1139,11 @@ impl PeerConfig { deserialize_image_quality, UserDefaultConfig::read().get("image_quality") ); + serde_field_string!( + default_scroll_mode, + deserialize_scroll_mode, + UserDefaultConfig::read().get("scroll_mode") + ); fn default_custom_image_quality() -> Vec { let f: f64 = UserDefaultConfig::read() @@ -1474,6 +1488,7 @@ impl UserDefaultConfig { } "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64), "custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0), + "scroll_mode" => self.get_string(key, "default", vec!["reverse"]), _ => self .options .get(key) diff --git a/src/client.rs b/src/client.rs index 0bfcfb655..f23a2c512 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1195,6 +1195,17 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save mouse scroll mode("default", "reverse") to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_scroll_mode(&mut self, value: String) { + let mut config = self.load_config(); + config.scroll_mode = value; + self.save_config(config); + } + /// Save scroll style to the current config. /// /// # Arguments diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index eaf273d2e..05dcf0343 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -21,8 +21,6 @@ use hbb_common::{ }; use std::{ collections::HashMap, - ffi::{CStr, CString}, - os::raw::c_char, str::FromStr, sync::{ atomic::{AtomicI32, Ordering}, @@ -302,6 +300,20 @@ pub fn session_set_keyboard_mode(session_id: SessionID, value: String) { } } +pub fn session_get_scroll_mode(session_id: SessionID) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&session_id) { + Some(session.get_scroll_mode()) + } else { + None + } +} + +pub fn session_set_scroll_mode(session_id: SessionID, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&session_id) { + session.save_scroll_mode(value); + } +} + pub fn session_get_custom_image_quality(session_id: SessionID) -> Option> { if let Some(session) = SESSIONS.read().unwrap().get(&session_id) { Some(session.get_custom_image_quality()) diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index a8304b5d0..c416b95d9 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,4 +1,4 @@ -use crate::input::{MOUSE_BUTTON_LEFT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}; +use crate::input::{MOUSE_BUTTON_LEFT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP, MOUSE_TYPE_WHEEL}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::{collections::HashMap, sync::atomic::AtomicBool}; use std::{ @@ -175,6 +175,14 @@ impl Session { self.lc.write().unwrap().save_keyboard_mode(value); } + pub fn get_scroll_mode(&self) -> String { + self.lc.read().unwrap().scroll_mode.clone() + } + + pub fn save_scroll_mode(&mut self, value: String) { + self.lc.write().unwrap().save_scroll_mode(value); + } + pub fn save_view_style(&mut self, value: String) { self.lc.write().unwrap().save_view_style(value); } @@ -730,6 +738,7 @@ impl Session { }); } "pan_update" => { + let (x, y) = self.get_scroll_xy((x, y)); touch_evt.set_pan_update(TouchPanUpdate { x, y, @@ -753,11 +762,26 @@ impl Session { send_pointer_device_event(evt, alt, ctrl, shift, command, self); } + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn is_scroll_reverse_mode(&self) -> bool { + self.lc.read().unwrap().scroll_mode.eq("reverse") + } + + #[inline] + fn get_scroll_xy(&self, xy: (i32, i32)) -> (i32, i32) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if self.is_scroll_reverse_mode() { + return (-xy.0, -xy.1); + } + xy + } + pub fn send_mouse( &self, mask: i32, - x: i32, - y: i32, + mut x: i32, + mut y: i32, alt: bool, ctrl: bool, shift: bool, @@ -772,6 +796,12 @@ impl Session { } } + let (x, y) = if mask == MOUSE_TYPE_WHEEL { + self.get_scroll_xy((x, y)) + } else { + (x, y) + }; + // #[cfg(not(any(target_os = "android", target_os = "ios")))] let (alt, ctrl, shift, command) = keyboard::client::get_modifiers_state(alt, ctrl, shift, command);