diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 6fe8d5946..cf5c4d161 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -718,7 +718,21 @@ class OverlayDialogManager { int _tagCount = 0; OverlayEntry? _mobileActionsOverlayEntry; - RxBool mobileActionsOverlayVisible = false.obs; + RxBool mobileActionsOverlayVisible = true.obs; + + setMobileActionsOverlayVisible(bool v, {store = true}) { + if (store) { + bind.setLocalFlutterOption(k: kOptionShowMobileAction, v: v ? 'Y' : 'N'); + } + // No need to read the value from local storage after setting it. + // It better to toggle the value directly. + mobileActionsOverlayVisible.value = v; + } + + loadMobileActionsOverlayVisible() { + mobileActionsOverlayVisible.value = + bind.getLocalFlutterOption(k: kOptionShowMobileAction) != 'N'; + } void setOverlayState(OverlayKeyState overlayKeyState) { _overlayKeyState = overlayKeyState; @@ -865,14 +879,14 @@ class OverlayDialogManager { ); overlayState.insert(overlay); _mobileActionsOverlayEntry = overlay; - mobileActionsOverlayVisible.value = true; + setMobileActionsOverlayVisible(true); } - void hideMobileActionsOverlay() { + void hideMobileActionsOverlay({store = true}) { if (_mobileActionsOverlayEntry != null) { _mobileActionsOverlayEntry!.remove(); _mobileActionsOverlayEntry = null; - mobileActionsOverlayVisible.value = false; + setMobileActionsOverlayVisible(false, store: store); return; } } @@ -891,21 +905,27 @@ class OverlayDialogManager { } makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) { - final position = SimpleWrapper(Offset(0, 0)); makeMobileActions(BuildContext context, double s) { final scale = s < 0.85 ? 0.85 : s; final session = ffi ?? gFFI; - // compute overlay position - final screenW = MediaQuery.of(context).size.width; - final screenH = MediaQuery.of(context).size.height; const double overlayW = 200; const double overlayH = 45; - final left = (screenW - overlayW * scale) / 2; - final top = screenH - (overlayH + 80) * scale; - position.value = Offset(left, top); + computeOverlayPosition() { + final screenW = MediaQuery.of(context).size.width; + final screenH = MediaQuery.of(context).size.height; + final left = (screenW - overlayW * scale) / 2; + final top = screenH - (overlayH + 80) * scale; + return Offset(left, top); + } + + if (draggablePositions.mobileActions.isInvalid()) { + draggablePositions.mobileActions.update(computeOverlayPosition()); + } else { + draggablePositions.mobileActions.tryAdjust(overlayW, overlayH, scale); + } return DraggableMobileActions( scale: scale, - position: position, + position: draggablePositions.mobileActions, width: overlayW, height: overlayH, onBackPressed: () => session.inputModel.tap(MouseButtons.right), diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index b16b4d9ad..9a6c5a4c9 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,6 +1,8 @@ import 'package:auto_size_text/auto_size_text.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; @@ -26,9 +28,12 @@ class DraggableChatWindow extends StatelessWidget { @override Widget build(BuildContext context) { + if (draggablePositions.chatWindow.isInvalid()) { + draggablePositions.chatWindow.update(position); + } return isIOS ? IOSDraggable( - position: position, + position: draggablePositions.chatWindow, chatModel: chatModel, width: width, height: height, @@ -45,7 +50,7 @@ class DraggableChatWindow extends StatelessWidget { ) : Draggable( checkKeyboard: true, - position: SimpleWrapper(position), + position: draggablePositions.chatWindow, width: width, height: height, chatModel: chatModel, @@ -176,7 +181,7 @@ class DraggableMobileActions extends StatelessWidget { required this.scale}); final double scale; - final SimpleWrapper position; + final DraggableKeyPosition position; final double width; final double height; final VoidCallback? onBackPressed; @@ -241,6 +246,92 @@ class DraggableMobileActions extends StatelessWidget { } } +class DraggableKeyPosition { + final String key; + Offset _pos; + late Debouncer _debouncerStore; + DraggableKeyPosition(this.key) + : _pos = DraggablePositions.kInvalidDraggablePosition; + + get pos => _pos; + + _loadPosition(String k) { + final value = bind.getLocalFlutterOption(k: k); + if (value.isNotEmpty) { + final parts = value.split(','); + if (parts.length == 2) { + return Offset(double.parse(parts[0]), double.parse(parts[1])); + } + } + return DraggablePositions.kInvalidDraggablePosition; + } + + load() { + _pos = _loadPosition(key); + _debouncerStore = Debouncer(const Duration(milliseconds: 500), + onChanged: (v) => _store(), initialValue: 0); + } + + update(Offset pos) { + _pos = pos; + _triggerStore(); + } + + // Adjust position to keep it in the screen + // Only used for desktop and web desktop + tryAdjust(double w, double h, double scale) { + final size = MediaQuery.of(Get.context!).size; + w = w * scale; + h = h * scale; + double x = _pos.dx; + double y = _pos.dy; + if (x + w > size.width) { + x = size.width - w; + } + final tabBarHeight = isDesktop ? kDesktopRemoteTabBarHeight : 0; + if (y + h > (size.height - tabBarHeight)) { + y = size.height - tabBarHeight - h; + } + if (x < 0) { + x = 0; + } + if (y < 0) { + y = 0; + } + if (x != _pos.dx || y != _pos.dy) { + update(Offset(x, y)); + } + } + + isInvalid() { + return _pos == DraggablePositions.kInvalidDraggablePosition; + } + + _triggerStore() => _debouncerStore.value = _debouncerStore.value + 1; + _store() { + bind.setLocalFlutterOption(k: key, v: '${_pos.dx},${_pos.dy}'); + } +} + +class DraggablePositions { + static const kChatWindow = 'draggablePositionChat'; + static const kMobileActions = 'draggablePositionMobile'; + static const kIOSDraggable = 'draggablePositionIOS'; + + static const kInvalidDraggablePosition = Offset(-999999, -999999); + final chatWindow = DraggableKeyPosition(kChatWindow); + final mobileActions = DraggableKeyPosition(kMobileActions); + final iOSDraggable = DraggableKeyPosition(kIOSDraggable); + + load() { + chatWindow.load(); + mobileActions.load(); + iOSDraggable.load(); + } +} + +DraggablePositions draggablePositions = DraggablePositions(); + class Draggable extends StatefulWidget { Draggable( {Key? key, @@ -255,7 +346,7 @@ class Draggable extends StatefulWidget { final bool checkKeyboard; final bool checkScreenSize; - final SimpleWrapper position; + final DraggableKeyPosition position; final double width; final double height; final ChatModel? chatModel; @@ -277,7 +368,7 @@ class _DraggableState extends State { _chatModel = widget.chatModel; } - get position => widget.position.value; + get position => widget.position.pos; void onPanUpdate(DragUpdateDetails d) { final offset = d.delta; @@ -301,7 +392,7 @@ class _DraggableState extends State { y = position.dy + offset.dy; } setState(() { - widget.position.value = Offset(x, y); + widget.position.update(Offset(x, y)); }); _chatModel?.setChatWindowPosition(position); } @@ -320,7 +411,7 @@ class _DraggableState extends State { // reset if (_lastBottomHeight > 0 && bottomHeight == 0) { setState(() { - widget.position.value = Offset(position.dx, _saveHeight); + widget.position.update(Offset(position.dx, _saveHeight)); }); } @@ -331,7 +422,7 @@ class _DraggableState extends State { if (sumHeight + position.dy > contextHeight) { final y = contextHeight - sumHeight; setState(() { - widget.position.value = Offset(position.dx, y); + widget.position.update(Offset(position.dx, y)); }); } } @@ -362,14 +453,14 @@ class _DraggableState extends State { class IOSDraggable extends StatefulWidget { const IOSDraggable( {Key? key, - this.position = Offset.zero, this.chatModel, + required this.position, required this.width, required this.height, required this.builder}) : super(key: key); - final Offset position; + final DraggableKeyPosition position; final ChatModel? chatModel; final double width; final double height; @@ -380,7 +471,6 @@ class IOSDraggable extends StatefulWidget { } class IOSDraggableState extends State { - late Offset _position; late ChatModel? _chatModel; late double _width; late double _height; @@ -391,25 +481,26 @@ class IOSDraggableState extends State { @override void initState() { super.initState(); - _position = widget.position; _chatModel = widget.chatModel; _width = widget.width; _height = widget.height; } + get position => widget.position; + checkKeyboard() { final bottomHeight = MediaQuery.of(context).viewInsets.bottom; final currentVisible = bottomHeight != 0; // save if (!_keyboardVisible && currentVisible) { - _saveHeight = _position.dy; + _saveHeight = position.value.dy; } // reset if (_lastBottomHeight > 0 && bottomHeight == 0) { setState(() { - _position = Offset(_position.dx, _saveHeight); + position.value = Offset(position.value.dx, _saveHeight); }); } @@ -417,10 +508,10 @@ class IOSDraggableState extends State { if (_keyboardVisible && currentVisible) { final sumHeight = bottomHeight + _height; final contextHeight = MediaQuery.of(context).size.height; - if (sumHeight + _position.dy > contextHeight) { + if (sumHeight + position.value.dy > contextHeight) { final y = contextHeight - sumHeight; setState(() { - _position = Offset(_position.dx, y); + position.value = Offset(position.value.dx, y); }); } } @@ -435,14 +526,14 @@ class IOSDraggableState extends State { return Stack( children: [ Positioned( - left: _position.dx, - top: _position.dy, + left: position.value.dx, + top: position.value.dy, child: GestureDetector( onPanUpdate: (details) { setState(() { - _position += details.delta; + position.value += details.delta; }); - _chatModel?.setChatWindowPosition(_position); + _chatModel?.setChatWindowPosition(position.value); }, child: Material( child: Container( diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 4568a3f02..ba93b7fdd 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -142,6 +142,8 @@ const String kOptionDisableFloatingWindow = "disable-floating-window"; const String kOptionKeepScreenOn = "keep-screen-on"; +const String kOptionShowMobileAction = "showMobileActions"; + const String kUrlActionClose = "close"; const String kTabLabelHomePage = "Home"; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 47a158682..ba5ef3bb1 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -134,6 +134,7 @@ class _RemotePageState extends State _ffi.ffiModel.updateEventListener(sessionId, widget.id); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + _ffi.dialogManager.loadMobileActionsOverlayVisible(); // Session option should be set after models.dart/FFI.start _showRemoteCursor.value = bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: 'show-remote-cursor'); @@ -322,13 +323,6 @@ class _RemotePageState extends State if (!_ffi.ffiModel.isPeerAndroid) { return Offstage(); } else { - if (_ffi.connType == ConnType.defaultConn && - _ffi.ffiModel.permissions['keyboard'] != false) { - Timer( - Duration(milliseconds: 10), - () => _ffi.dialogManager - .mobileActionsOverlayVisible.value = true); - } return Obx(() => Offstage( offstage: _ffi.dialogManager .mobileActionsOverlayVisible.isFalse, diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 1c90e25d7..e75d75c23 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -579,8 +579,8 @@ class _MobileActionMenu extends StatelessWidget { return Obx(() => _IconMenuButton( assetName: 'assets/actions_mobile.svg', tooltip: 'Mobile Actions', - onPressed: () => - ffi.dialogManager.mobileActionsOverlayVisible.toggle(), + onPressed: () => ffi.dialogManager.setMobileActionsOverlayVisible( + !ffi.dialogManager.mobileActionsOverlayVisible.value), color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 7e89d5153..0386d63cf 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -6,6 +6,7 @@ import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/overlay.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; @@ -156,6 +157,7 @@ void runMobileApp() async { await initEnv(kAppTypeMain); if (isAndroid) androidChannelInit(); if (isAndroid) platformFFI.syncAndroidServiceAppDirConfigPath(); + draggablePositions.load(); await Future.wait([gFFI.abModel.loadCache(), gFFI.groupModel.loadCache()]); gFFI.userModel.refreshCurrentUser(); runApp(App()); @@ -176,6 +178,7 @@ void runMultiWindow( late Widget widget; switch (appType) { case kAppTypeDesktopRemote: + draggablePositions.load(); widget = DesktopRemoteScreen( params: argument, ); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 4d021609a..c9a9daf61 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -82,13 +82,14 @@ class _RemotePageState extends State { .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; _blockableOverlayState.applyFfi(gFFI); + gFFI.dialogManager.loadMobileActionsOverlayVisible(); } @override Future dispose() async { // https://github.com/flutter/flutter/issues/64935 super.dispose(); - gFFI.dialogManager.hideMobileActionsOverlay(); + gFFI.dialogManager.hideMobileActionsOverlay(store: false); gFFI.inputModel.listenToMouse(false); gFFI.imageModel.disposeImage(); gFFI.cursorModel.disposeImages(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 2ea037609..e1b024f0d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -917,10 +917,12 @@ class FfiModel with ChangeNotifier { if (parent.target?.connType == ConnType.defaultConn && parent.target != null && parent.target!.ffiModel.permissions['keyboard'] != false) { - Timer( - Duration(milliseconds: delayMSecs), - () => parent.target!.dialogManager - .showMobileActionsOverlay(ffi: parent.target!)); + Timer(Duration(milliseconds: delayMSecs), () { + if (parent.target!.dialogManager.mobileActionsOverlayVisible.isTrue) { + parent.target!.dialogManager + .showMobileActionsOverlay(ffi: parent.target!); + } + }); } } } @@ -1587,10 +1589,13 @@ class CanvasModel with ChangeNotifier { // (focalPoint.dx - _x_1) / s1 + displayOriginX = (focalPoint.dx - _x_2) / s2 + displayOriginX // _x_2 = focalPoint.dx - (focalPoint.dx - _x_1) / s1 * s2 _x = focalPoint.dx - (focalPoint.dx - _x) / s * _scale; - final adjustForKeyboard = parent.target?.cursorModel.adjustForKeyboard() ?? 0.0; + final adjustForKeyboard = + parent.target?.cursorModel.adjustForKeyboard() ?? 0.0; // (focalPoint.dy - _y_1 + adjust) / s1 + displayOriginY = (focalPoint.dy - _y_2 + adjust) / s2 + displayOriginY // _y_2 = focalPoint.dy + adjust - (focalPoint.dy - _y_1 + adjust) / s1 * s2 - _y = focalPoint.dy + adjustForKeyboard - (focalPoint.dy - _y + adjustForKeyboard) / s * _scale; + _y = focalPoint.dy + + adjustForKeyboard - + (focalPoint.dy - _y + adjustForKeyboard) / s * _scale; notifyListeners(); }