diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index d84789d9c..3e248700f 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,7 +1,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -92,31 +92,30 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Obx(() => Opacity( - opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, child: Row(children: [ Icon(Icons.chat_bubble_outline, size: 20, color: Theme.of(context).colorScheme.primary), SizedBox(width: 6), Text(translate("Chat")) - ])), - Padding( - padding: EdgeInsets.all(2), - child: ActionIcon( - message: 'Close', - icon: IconFont.close, - onTap: chatModel.hideChatWindowOverlay, - isClose: true, - boxSize: 32, - )) - ], - ))), + ])))), + Padding( + padding: EdgeInsets.all(2), + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ), ); } } @@ -372,3 +371,68 @@ class QualityMonitor extends StatelessWidget { ) : const SizedBox.shrink())); } + +class PenetrableOverlayState { + final _middleBlocked = false.obs; + final _overlayKey = GlobalKey(); + + VoidCallback? onMiddleBlockedClick; // to-do use listener + + RxBool get middleBlocked => _middleBlocked; + GlobalKey get overlayKey => _overlayKey; + OverlayState? get overlayState => _overlayKey.currentState; + + OverlayState? getOverlayStateOrGlobal() { + if (overlayState == null) { + if (globalKey.currentState == null || + globalKey.currentState!.overlay == null) return null; + return globalKey.currentState!.overlay; + } else { + return overlayState; + } + } + + void addMiddleBlockedListener(void Function(bool) cb) { + _middleBlocked.listen(cb); + } + + void setMiddleBlocked(bool blocked) { + if (blocked != _middleBlocked.value) { + _middleBlocked.value = blocked; + } + } +} + +class PenetrableOverlay extends StatelessWidget { + final Widget underlying; + final List? upperLayer; + + final PenetrableOverlayState state; + + PenetrableOverlay( + {required this.underlying, required this.state, this.upperLayer}); + + @override + Widget build(BuildContext context) { + final initialEntries = [ + OverlayEntry(builder: (_) => underlying), + + /// middle layer + OverlayEntry( + builder: (context) => Obx(() => Listener( + onPointerDown: (_) { + state.onMiddleBlockedClick?.call(); + }, + child: Container( + color: state.middleBlocked.value + ? Colors.red.withOpacity(0.3) + : null)))), + ]; + + if (upperLayer != null) { + initialEntries.addAll(upperLayer!); + } + + return Overlay(key: state.overlayKey, initialEntries: initialEntries); + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 2e4668159..4bda68c2d 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,6 +62,8 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + final overlayState = PenetrableOverlayState(); + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -133,6 +135,12 @@ class _RemotePageState extends State // }); // _isCustomCursorInited = true; // } + + _ffi.chatModel.setPenetrableOverlayState(overlayState); + // make remote page penetrable automatically, effective for chat over remote + overlayState.onMiddleBlockedClick = () { + overlayState.setMiddleBlocked(false); + }; } @override @@ -192,39 +200,47 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.chatModel.setOverlayState(Overlay.of(context)); - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return Container( - color: Colors.black, - child: RawKeyFocusScope( - focusNode: _rawKeyFocusNode, - onFocusChange: (bool imageFocused) { - debugPrint( - "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); - // See [onWindowBlur]. - if (Platform.isWindows) { - if (_isWindowBlur) { - imageFocused = false; - Future.delayed(Duration.zero, () { - _rawKeyFocusNode.unfocus(); - }); - } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); - } - } - }, - inputModel: _ffi.inputModel, - child: getBodyForDesktop(context))); - }) - ], - )); + backgroundColor: Theme.of(context).backgroundColor, + body: PenetrableOverlay( + state: overlayState, + underlying: Container( + color: Colors.black, + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (Platform.isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + upperLayer: [ + OverlayEntry( + builder: (context) => RemoteMenubar( + id: widget.id, + ffi: _ffi, + state: widget.menubarState, + onEnterOrLeaveImageSetter: (func) => + _onEnterOrLeaveImage4Menubar = func, + onEnterOrLeaveImageCleaner: () => + _onEnterOrLeaveImage4Menubar = null, + )) + ], + ), + ); } @override @@ -345,13 +361,6 @@ class _RemotePageState extends State QualityMonitor(_ffi.qualityMonitorModel), null, null), ), ); - paints.add(RemoteMenubar( - id: widget.id, - ffi: _ffi, - state: widget.menubarState, - onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, - onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, - )); return Stack( children: paints, ); diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index 64d289fcc..6ad030464 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -297,12 +297,23 @@ class _RemoteMenubarState extends State { ); } + final _chatButtonKey = GlobalKey(); Widget _buildChat(BuildContext context) { return IconButton( + key: _chatButtonKey, tooltip: translate('Chat'), onPressed: () { + RenderBox? renderBox = + _chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); + } + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); }, icon: const Icon( Icons.message, diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index dd35bd22f..b61ce79a7 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -5,7 +5,6 @@ import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; -import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -30,16 +29,12 @@ class MessageBody { class ChatModel with ChangeNotifier { static final clientModeID = -1; - /// _overlayState: - /// Desktop: store session overlay by using [setOverlayState]. - /// Mobile: always null, use global overlay. - /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay] - OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; RxBool isWindowFocus = true.obs; + PenetrableOverlayState? pOverlayState; final ChatUser me = ChatUser( id: "", @@ -58,6 +53,19 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; + void setPenetrableOverlayState(PenetrableOverlayState state) { + pOverlayState = state; + + pOverlayState!.addMiddleBlockedListener((v) { + if (!v) { + isWindowFocus.value = false; + if (isWindowFocus.value) { + isWindowFocus.toggle(); + } + } + }); + } + final WeakReference parent; ChatModel(this.parent); @@ -74,20 +82,6 @@ class ChatModel with ChangeNotifier { } } - setOverlayState(OverlayState? os) { - _overlayState = os; - } - - OverlayState? _getOverlayState() { - if (_overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return _overlayState; - } - } - showChatIconOverlay({Offset offset = const Offset(200, 50)}) { if (chatIconOverlayEntry != null) { chatIconOverlayEntry!.remove(); @@ -100,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = _getOverlayState(); + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -132,33 +126,26 @@ class ChatModel with ChangeNotifier { } } - showChatWindowOverlay() { + showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; - final overlayState = _getOverlayState(); + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); + + final overlayState = pOverlayState?.getOverlayStateOrGlobal(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - bool innerClicked = false; return Listener( onPointerDown: (_) { - if (!innerClicked) { - isWindowFocus.value = false; + if (!isWindowFocus.value) { + isWindowFocus.value = true; + pOverlayState?.setMiddleBlocked(true); } - innerClicked = false; }, - child: Obx(() => Container( - color: isWindowFocus.value ? Colors.red.withOpacity(0.3) : null, - child: Listener( - onPointerDown: (_) { - innerClicked = true; - if (!isWindowFocus.value) { - isWindowFocus.value = true; - } - }, - child: DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this))))); + child: DraggableChatWindow( + position: chatInitPos ?? Offset(20, 80), + width: 250, + height: 350, + chatModel: this)); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; @@ -167,6 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { + pOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; @@ -176,13 +164,13 @@ class ChatModel with ChangeNotifier { _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || chatWindowOverlayEntry == null); - toggleChatOverlay() { + toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); if (!isDesktop) { showChatIconOverlay(); } - showChatWindowOverlay(); + showChatWindowOverlay(chatInitPos: chatInitPos); } else { hideChatIconOverlay(); hideChatWindowOverlay(); @@ -310,7 +298,6 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); - _overlayState = null; notifyListeners(); }