diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart new file mode 100644 index 000000000..ad50d4839 --- /dev/null +++ b/flutter/lib/common/widgets/remote_input.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../models/input_model.dart'; + +class RawKeyFocusScope extends StatelessWidget { + final FocusNode? focusNode; + final ValueChanged? onFocusChange; + final InputModel inputModel; + final Widget child; + + RawKeyFocusScope( + {this.focusNode, + this.onFocusChange, + required this.inputModel, + required this.child}); + + @override + Widget build(BuildContext context) { + return FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: focusNode, + onFocusChange: onFocusChange, + onKey: inputModel.handleRawKeyEvent, + child: child)); + } +} + +class RawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final MouseCursor? cursor; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + + RawPointerMouseRegion( + {this.onEnter, + this.onExit, + this.cursor, + required this.inputModel, + required this.child}); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: inputModel.onPointHoverImage, + onPointerDown: inputModel.onPointDownImage, + onPointerUp: inputModel.onPointUpImage, + onPointerMove: inputModel.onPointMoveImage, + onPointerSignal: inputModel.onPointerSignalImage, + child: MouseRegion( + cursor: cursor ?? MouseCursor.defer, + onEnter: onEnter, + onExit: onExit, + child: child)); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3d6f3ee21..8f3644ded 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -10,6 +10,7 @@ import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; +import '../../common/widgets/remote_input.dart'; import '../widgets/remote_menubar.dart'; import '../../common.dart'; import '../../mobile/widgets/dialog.dart'; @@ -135,7 +136,13 @@ class _RemotePageState extends State _ffi.dialogManager.setOverlayState(Overlay.of(context)); return Container( color: Colors.black, - child: getRawPointerAndKeyBody(getBodyForDesktop(context))); + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool v) { + _imageFocused = v; + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))); }) ], )); @@ -159,20 +166,6 @@ class _RemotePageState extends State ], child: buildBody(context))); } - Widget getRawPointerAndKeyBody(Widget child) { - return FocusScope( - autofocus: true, - child: Focus( - autofocus: true, - canRequestFocus: true, - focusNode: _rawKeyFocusNode, - onFocusChange: (bool v) { - _imageFocused = v; - }, - onKey: _ffi.inputModel.handleRawKeyEvent, - child: child)); - } - void enterView(PointerEnterEvent evt) { if (!_imageFocused) { _rawKeyFocusNode.requestFocus(); @@ -200,17 +193,6 @@ class _RemotePageState extends State _ffi.inputModel.enterOrLeave(false); } - Widget _buildImageListener(Widget child) { - return Listener( - onPointerHover: _ffi.inputModel.onPointHoverImage, - onPointerDown: _ffi.inputModel.onPointDownImage, - onPointerUp: _ffi.inputModel.onPointUpImage, - onPointerMove: _ffi.inputModel.onPointMoveImage, - onPointerSignal: _ffi.inputModel.onPointerSignalImage, - child: - MouseRegion(onEnter: enterView, onExit: leaveView, child: child)); - } - Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { @@ -226,7 +208,12 @@ class _RemotePageState extends State cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, - listenerBuilder: _buildImageListener, + listenerBuilder: (child) => RawPointerMouseRegion( + onEnter: enterView, + onExit: leaveView, + inputModel: _ffi.inputModel, + child: child, + ), ); })) ]; diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 5be604ee7..30b52b141 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -7,11 +7,12 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; -import '../../consts.dart'; +import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -26,7 +27,7 @@ class RemotePage extends StatefulWidget { final String id; @override - _RemotePageState createState() => _RemotePageState(); + State createState() => _RemotePageState(); } class _RemotePageState extends State { @@ -44,9 +45,8 @@ class _RemotePageState extends State { final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); var _showEdit = false; // use soft keyboard - var _isPhysicalMouse = false; - get inputModel => gFFI.inputModel; + InputModel get inputModel => gFFI.inputModel; @override void initState() { @@ -148,8 +148,8 @@ class _RemotePageState extends State { } return; } - if (oldValue.length > 0 && - newValue.length > 0 && + if (oldValue.isNotEmpty && + newValue.isNotEmpty && oldValue[0] == '\1' && newValue[0] != '\1') { // clipboard @@ -214,14 +214,6 @@ class _RemotePageState extends State { }); } - void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { - // for maximum compatibility - final label = logicalKeyMap[e.logicalKey.keyId] ?? - physicalKeyMap[e.physicalKey.usbHidUsage] ?? - e.logicalKey.keyLabel; - inputModel.inputKey(label, down: down, press: press ?? false); - } - @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; @@ -234,159 +226,68 @@ class _RemotePageState extends State { clientClose(gFFI.dialogManager); return false; }, - child: getRawPointerAndKeyBody( - keyboard, - Scaffold( - // resizeToAvoidBottomInset: true, - floatingActionButton: !showActionButton - ? null - : FloatingActionButton( - mini: !hideKeyboard, - child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), - backgroundColor: MyTheme.accent, - onPressed: () { - setState(() { - if (hideKeyboard) { - _showEdit = false; - gFFI.invokeMethod("enable_soft_keyboard", false); - _mobileFocusNode.unfocus(); - _physicalFocusNode.requestFocus(); - } else { - _showBar = !_showBar; - } - }); - }), - bottomNavigationBar: _showBar && pi.displays.length > 0 - ? getBottomAppBar(keyboard) - : null, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - return Container( - color: Colors.black, - child: isWebDesktop - ? getBodyForDesktopWithListener(keyboard) - : SafeArea(child: - OrientationBuilder(builder: (ctx, orientation) { - if (_currentOrientation != orientation) { - Timer(const Duration(milliseconds: 200), () { - gFFI.dialogManager - .resetMobileActionsOverlay(ffi: gFFI); - _currentOrientation = orientation; - gFFI.canvasModel.updateViewStyle(); - }); - } - return Container( - color: MyTheme.canvasColor, - child: _isPhysicalMouse - ? getBodyForMobile() - : getBodyForMobileWithGesture()); - }))); - }) - ], - ))), + child: getRawPointerAndKeyBody(Scaffold( + // resizeToAvoidBottomInset: true, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !hideKeyboard, + child: Icon( + hideKeyboard ? Icons.expand_more : Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (hideKeyboard) { + _showEdit = false; + gFFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: _showBar && pi.displays.isNotEmpty + ? getBottomAppBar(keyboard) + : null, + body: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: Colors.black, + child: isWebDesktop + ? getBodyForDesktopWithListener(keyboard) + : SafeArea(child: + OrientationBuilder(builder: (ctx, orientation) { + if (_currentOrientation != orientation) { + Timer(const Duration(milliseconds: 200), () { + gFFI.dialogManager + .resetMobileActionsOverlay(ffi: gFFI); + _currentOrientation = orientation; + gFFI.canvasModel.updateViewStyle(); + }); + } + return Obx(() => Container( + color: MyTheme.canvasColor, + child: inputModel.isPhysicalMouse.value + ? getBodyForMobile() + : getBodyForMobileWithGesture())); + }))); + }) + ], + ))), ); } - Widget getRawPointerAndKeyBody(bool keyboard, Widget child) { - return Listener( - onPointerHover: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (!_isPhysicalMouse) { - setState(() { - _isPhysicalMouse = true; - }); - } - if (_isPhysicalMouse) { - inputModel.handleMouse(getEvent(e, 'mousemove')); - } - }, - onPointerDown: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) { - if (_isPhysicalMouse) { - setState(() { - _isPhysicalMouse = false; - }); - } - } - if (_isPhysicalMouse) { - inputModel.handleMouse(getEvent(e, 'mousedown')); - } - }, - onPointerUp: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { - inputModel.handleMouse(getEvent(e, 'mouseup')); - } - }, - onPointerMove: (e) { - if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { - inputModel.handleMouse(getEvent(e, 'mousemove')); - } - }, - onPointerSignal: (e) { - if (e is PointerScrollEvent) { - var dy = 0; - if (e.scrollDelta.dy > 0) { - dy = -1; - } else if (e.scrollDelta.dy < 0) { - dy = 1; - } - inputModel.scroll(dy); - } - }, - child: MouseRegion( - cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, - child: FocusScope( - autofocus: true, - child: Focus( - autofocus: true, - canRequestFocus: true, - focusNode: _physicalFocusNode, - onKey: (data, e) { - final key = e.logicalKey; - if (e is RawKeyDownEvent) { - if (e.repeat && - !e.isAltPressed && - !e.isControlPressed && - !e.isShiftPressed && - !e.isMetaPressed) { - sendRawKey(e, press: true); - } else { - sendRawKey(e, down: true); - if (e.isAltPressed && !inputModel.alt) { - inputModel.alt = true; - } else if (e.isControlPressed && !inputModel.ctrl) { - inputModel.ctrl = true; - } else if (e.isShiftPressed && !inputModel.shift) { - inputModel.shift = true; - } else if (e.isMetaPressed && !inputModel.command) { - inputModel.command = true; - } - } - } - // [!_showEdit] workaround for soft-keyboard's control_key like Backspace / Enter - if (!_showEdit && e is RawKeyUpEvent) { - if (key == LogicalKeyboardKey.altLeft || - key == LogicalKeyboardKey.altRight) { - inputModel.alt = false; - } else if (key == LogicalKeyboardKey.controlLeft || - key == LogicalKeyboardKey.controlRight) { - inputModel.ctrl = false; - } else if (key == LogicalKeyboardKey.shiftRight || - key == LogicalKeyboardKey.shiftLeft) { - inputModel.shift = false; - } else if (key == LogicalKeyboardKey.metaLeft || - key == LogicalKeyboardKey.metaRight) { - inputModel.command = false; - } - sendRawKey(e); - } - return KeyEventResult.handled; - }, - child: child)))); + Widget getRawPointerAndKeyBody(Widget child) { + final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; + return RawPointerMouseRegion( + cursor: keyboard ? SystemMouseCursors.none : MouseCursor.defer, + inputModel: inputModel, + child: RawKeyFocusScope( + focusNode: _physicalFocusNode, + inputModel: inputModel, + child: child)); } Widget getBottomAppBar(bool keyboard) { @@ -685,7 +586,7 @@ class _RemotePageState extends State { if (perms['keyboard'] != false) { if (pi.platform == 'Linux' || pi.sasEnabled) { more.add(PopupMenuItem( - child: Text(translate('Insert') + ' Ctrl + Alt + Del'), + child: Text('${translate('Insert')} Ctrl + Alt + Del'), value: 'cad')); } more.add(PopupMenuItem( @@ -694,8 +595,8 @@ class _RemotePageState extends State { await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( - child: Text(translate((gFFI.ffiModel.inputBlocked ? 'Unb' : 'B') + - 'lock user input')), + child: Text(translate( + '${gFFI.ffiModel.inputBlocked ? 'Unb' : 'B'}lock user input')), value: 'block-input')); } } @@ -720,7 +621,7 @@ class _RemotePageState extends State { } else if (value == 'block-input') { bind.sessionToggleOption( id: widget.id, - value: (gFFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + value: '${gFFI.ffiModel.inputBlocked ? 'un' : ''}block-input'); gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked; } else if (value == 'refresh') { bind.sessionRefresh(id: widget.id); @@ -845,9 +746,9 @@ class _RemotePageState extends State { SizedBox(width: 9999), ]; for (var i = 1; i <= 12; ++i) { - final name = 'F' + i.toString(); + final name = 'F$i'; fn.add(wrap(name, () { - inputModel.inputKey('VK_' + name); + inputModel.inputKey('VK_$name'); })); } final more = [ @@ -920,7 +821,7 @@ class ImagePaint extends StatelessWidget { final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( - painter: new ImagePainter( + painter: ImagePainter( image: m.image, x: c.x / s, y: (c.y - adjust) / s, scale: s), ); } @@ -934,7 +835,7 @@ class CursorPaint extends StatelessWidget { final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( - painter: new ImagePainter( + painter: ImagePainter( image: m.image, x: m.x * s - m.hotx + c.x, y: m.y * s - m.hoty + c.y - adjust, @@ -960,7 +861,7 @@ class ImagePainter extends CustomPainter { void paint(Canvas canvas, Size size) { if (image == null) return; canvas.scale(scale, scale); - canvas.drawImage(image!, new Offset(x, y), new Paint()); + canvas.drawImage(image!, Offset(x, y), Paint()); } @override diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0274f2884..4f71f591b 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -5,7 +5,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; -import 'package:get/get_core/src/get_main.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -40,7 +39,7 @@ class InputModel { var command = false; // mouse - var _isPhysicalMouse = false; + final isPhysicalMouse = false.obs; int _lastMouseDownButtons = 0; get id => parent.target?.id ?? ""; @@ -245,10 +244,10 @@ class InputModel { void onPointHoverImage(PointerHoverEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; - if (!_isPhysicalMouse) { - _isPhysicalMouse = true; + if (!isPhysicalMouse.value) { + isPhysicalMouse.value = true; } - if (_isPhysicalMouse) { + if (isPhysicalMouse.value) { handleMouse(getEvent(e, 'mousemove')); } } @@ -256,25 +255,25 @@ class InputModel { void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); if (e.kind != ui.PointerDeviceKind.mouse) { - if (_isPhysicalMouse) { - _isPhysicalMouse = false; + if (isPhysicalMouse.value) { + isPhysicalMouse.value = false; } } - if (_isPhysicalMouse) { + if (isPhysicalMouse.value) { handleMouse(getEvent(e, 'mousedown')); } } void onPointUpImage(PointerUpEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { + if (isPhysicalMouse.value) { handleMouse(getEvent(e, 'mouseup')); } } void onPointMoveImage(PointerMoveEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; - if (_isPhysicalMouse) { + if (isPhysicalMouse.value) { handleMouse(getEvent(e, 'mousemove')); } }