From 1e9625045b222fd9d63013b37ceb88944ac5b6d9 Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 20:05:57 +0900 Subject: [PATCH 1/8] fix chat text selectable --- flutter/lib/common/widgets/chat_page.dart | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index d1d96199a..62f81b797 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -95,10 +95,31 @@ class ChatPage extends StatelessWidget implements PageShape { color: Theme.of(context).colorScheme.primary)), messageOptions: MessageOptions( showOtherUsersAvatar: false, - showTime: true, - currentUserTextColor: Colors.white, textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, + messageTextBuilder: (message, _, __) { + final isOwnMessage = + message.user.id == currentUser.id; + return Column( + crossAxisAlignment: isOwnMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Text(message.text, + style: TextStyle(color: Colors.white)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + "${message.createdAt.hour}:${message.createdAt.minute}", + style: TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ], + ); + }, messageDecorationBuilder: (_, __, ___) => defaultMessageDecoration( color: MyTheme.accent80, From c6269b54af37e60fb4a03e6f06623065ea998a0e Mon Sep 17 00:00:00 2001 From: csf Date: Thu, 2 Feb 2023 21:39:25 +0900 Subject: [PATCH 2/8] add requestChatInputFocus() --- flutter/lib/models/chat_model.dart | 12 ++++++++++++ flutter/test/cm_test.dart | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..bab88a9dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; @@ -139,6 +141,7 @@ class ChatModel with ChangeNotifier { }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; + requestChatInputFocus(); } hideChatWindowOverlay() { @@ -188,6 +191,7 @@ class ChatModel with ChangeNotifier { await windowManager.setSizeAlignment( kConnectionManagerWindowSize, Alignment.topRight); } else { + requestChatInputFocus(); await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; @@ -292,4 +296,12 @@ class ChatModel with ChangeNotifier { resetClientMode() { _messages[clientModeID]?.clear(); } + + void requestChatInputFocus() { + Timer(Duration(milliseconds: 100), () { + if (inputNode.hasListeners && inputNode.canRequestFocus) { + inputNode.requestFocus(); + } + }); + } } diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 592a28fcf..2c037c7b0 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -16,7 +16,7 @@ final testClients = [ Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) ]; -/// -t lib/cm_main.dart to test cm +/// flutter run -d {platform} -t lib/cm_test.dart to test cm void main(List args) async { isTest = true; WidgetsFlutterBinding.ensureInitialized(); From c306ec3ba76217f24d66384f31c1d8fecb388291 Mon Sep 17 00:00:00 2001 From: csf Date: Mon, 6 Feb 2023 09:54:21 +0900 Subject: [PATCH 3/8] opt chat window on its overlay, make window focusable as a desktop app --- flutter/lib/common/widgets/overlay.dart | 64 ++++++++++++++----------- flutter/lib/models/chat_model.dart | 31 ++++++++++-- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 4b4172ffd..d84789d9c 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,6 +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:provider/provider.dart'; import '../../consts.dart'; @@ -91,28 +92,31 @@ class DraggableChatWindow extends StatelessWidget { bottom: BorderSide( color: Theme.of(context).hintColor.withOpacity(0.4)))), height: 38, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - 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, - )) - ], - ), + 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(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, + )) + ], + ))), ); } } @@ -304,15 +308,17 @@ class _DraggableState extends State { if (widget.checkKeyboard) { checkKeyboard(); } - if (widget.checkKeyboard) { + if (widget.checkScreenSize) { checkScreenSize(); } - return Positioned( - top: _position.dy, - left: _position.dx, - width: widget.width, - height: widget.height, - child: widget.builder(context, onPanUpdate)); + return Stack(children: [ + Positioned( + top: _position.dy, + left: _position.dx, + width: widget.width, + height: widget.height, + child: widget.builder(context, onPanUpdate)) + ]); } } diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index bab88a9dd..dd35bd22f 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -4,6 +4,8 @@ import 'package:dash_chat_2/dash_chat_2.dart'; 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'; @@ -37,6 +39,8 @@ class ChatModel with ChangeNotifier { OverlayEntry? chatWindowOverlayEntry; bool isConnManager = false; + RxBool isWindowFocus = true.obs; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -133,11 +137,28 @@ class ChatModel with ChangeNotifier { final overlayState = _getOverlayState(); if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this); + bool innerClicked = false; + return Listener( + onPointerDown: (_) { + if (!innerClicked) { + isWindowFocus.value = false; + } + 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))))); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; From 893f18cdec1b4fedf72af67bbfb7fe03e047db11 Mon Sep 17 00:00:00 2001 From: csf Date: Tue, 7 Feb 2023 00:11:48 +0900 Subject: [PATCH 4/8] add PenetrableOverlayState, opt chat page over remote_page --- flutter/lib/common/widgets/overlay.dart | 106 ++++++++++++++---- flutter/lib/desktop/pages/remote_page.dart | 89 ++++++++------- .../lib/desktop/widgets/remote_menubar.dart | 13 ++- flutter/lib/models/chat_model.dart | 75 +++++-------- 4 files changed, 177 insertions(+), 106 deletions(-) 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(); } From 0dba0130893b54951fe3df3b9ce4997037a0a7ca Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 21:54:48 +0900 Subject: [PATCH 5/8] remove unused Overlay in desktop_tab_page.dart and server_page.dart --- .../lib/desktop/pages/desktop_tab_page.dart | 28 +++++++--------- flutter/lib/desktop/pages/server_page.dart | 33 ++++++++----------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index c1965921c..35d5a61ef 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -64,23 +64,17 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final tabWidget = Container( - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - onTap: DesktopTabPage.onAddSetting, - isClose: false, - ), - )); - }) - ]), - ); + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: DesktopTab( + controller: tabController, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: DesktopTabPage.onAddSetting, + isClose: false, + ), + ))); return Platform.isMacOS ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 521413647..b4d7f4fac 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -68,26 +68,19 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - ], - ), - ), - ); - }) - ]), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).backgroundColor, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + ], + ), + ), + )))); } @override From 3d5aca18d690235ec1fb361b8526a25af7d46672 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:01:15 +0900 Subject: [PATCH 6/8] refactor OverlayKeyState for OverlayDialogManager and ChatModel --- flutter/lib/common.dart | 30 +++++++++++-------- flutter/lib/common/widgets/overlay.dart | 24 ++++----------- .../lib/desktop/pages/file_manager_page.dart | 7 +++-- flutter/lib/desktop/pages/remote_page.dart | 16 ++++++---- flutter/lib/models/chat_model.dart | 18 +++++------ 5 files changed, 47 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index a2623ff15..04e29eaa0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -369,20 +369,25 @@ class Dialog { } } +class OverlayKeyState { + final _overlayKey = GlobalKey(); + + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + class OverlayDialogManager { - OverlayState? _overlayState; final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); int _tagCount = 0; OverlayEntry? _mobileActionsOverlayEntry; - /// By default OverlayDialogManager use global overlay - OverlayDialogManager() { - _overlayState = globalKey.currentState?.overlay; - } - - void setOverlayState(OverlayState? overlayState) { - _overlayState = overlayState; + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; } void dismissAll() { @@ -406,7 +411,7 @@ class OverlayDialogManager { bool useAnimation = true, bool forceGlobal = false}) { final overlayState = - forceGlobal ? globalKey.currentState?.overlay : _overlayState; + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; if (overlayState == null) { return Future.error( @@ -510,7 +515,8 @@ class OverlayDialogManager { void showMobileActionsOverlay({FFI? ffi}) { if (_mobileActionsOverlayEntry != null) return; - if (_overlayState == null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; // compute overlay position final screenW = MediaQuery.of(globalKey.currentContext!).size.width; @@ -536,7 +542,7 @@ class OverlayDialogManager { onHidePressed: () => hideMobileActionsOverlay(), ); }); - _overlayState!.insert(overlay); + overlayState.insert(overlay); _mobileActionsOverlayEntry = overlay; } @@ -1701,4 +1707,4 @@ Future updateSystemWindowTheme() async { : SystemWindowTheme.dark); } } -} \ No newline at end of file +} diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 3e248700f..32dced02a 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -372,25 +372,12 @@ class QualityMonitor extends StatelessWidget { : const SizedBox.shrink())); } -class PenetrableOverlayState { +class BlockableOverlayState extends OverlayKeyState { 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); @@ -403,13 +390,13 @@ class PenetrableOverlayState { } } -class PenetrableOverlay extends StatelessWidget { +class BlockableOverlay extends StatelessWidget { final Widget underlying; final List? upperLayer; - final PenetrableOverlayState state; + final BlockableOverlayState state; - PenetrableOverlay( + BlockableOverlay( {required this.underlying, required this.state, this.upperLayer}); @override @@ -433,6 +420,7 @@ class PenetrableOverlay extends StatelessWidget { initialEntries.addAll(upperLayer!); } - return Overlay(key: state.overlayKey, initialEntries: initialEntries); + /// set key + return Overlay(key: state.key, initialEntries: initialEntries); } } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index b6a9e5fed..9955c2768 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -80,6 +80,7 @@ class _FileManagerPageState extends State Entry? _lastClickEntry; final _dropMaskVisible = false.obs; // TODO impl drop mask + final _overlayKeyState = OverlayKeyState(); ScrollController getBreadCrumbScrollController(bool isLocal) { return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; @@ -115,6 +116,7 @@ class _FileManagerPageState extends State // register location listener _locationNodeLocal.addListener(onLocalLocationFocusChanged); _locationNodeRemote.addListener(onRemoteLocationFocusChanged); + _ffi.dialogManager.setOverlayState(_overlayKeyState); } @override @@ -137,9 +139,8 @@ class _FileManagerPageState extends State @override Widget build(BuildContext context) { super.build(context); - return Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Overlay(key: _overlayKeyState.key, initialEntries: [ + OverlayEntry(builder: (_) { return ChangeNotifierProvider.value( value: _ffi.fileModel, child: Consumer(builder: (context, model, child) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 4bda68c2d..c444d1f5f 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -62,7 +62,7 @@ class _RemotePageState extends State late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final overlayState = PenetrableOverlayState(); + final _blockableOverlayState = BlockableOverlayState(); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); @@ -136,10 +136,11 @@ class _RemotePageState extends State // _isCustomCursorInited = true; // } - _ffi.chatModel.setPenetrableOverlayState(overlayState); + _ffi.dialogManager.setOverlayState(_blockableOverlayState); + _ffi.chatModel.setOverlayState(_blockableOverlayState); // make remote page penetrable automatically, effective for chat over remote - overlayState.onMiddleBlockedClick = () { - overlayState.setMiddleBlocked(false); + _blockableOverlayState.onMiddleBlockedClick = () { + _blockableOverlayState.setMiddleBlocked(false); }; } @@ -201,8 +202,11 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).backgroundColor, - body: PenetrableOverlay( - state: overlayState, + + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + body: BlockableOverlay( + state: _blockableOverlayState, underlying: Container( color: Colors.black, child: RawKeyFocusScope( diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index b61ce79a7..8320d08dd 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -34,7 +34,7 @@ class ChatModel with ChangeNotifier { bool isConnManager = false; RxBool isWindowFocus = true.obs; - PenetrableOverlayState? pOverlayState; + BlockableOverlayState? _blockableOverlayState; final ChatUser me = ChatUser( id: "", @@ -53,10 +53,10 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; - void setPenetrableOverlayState(PenetrableOverlayState state) { - pOverlayState = state; + void setOverlayState(BlockableOverlayState blockableOverlayState) { + _blockableOverlayState = blockableOverlayState; - pOverlayState!.addMiddleBlockedListener((v) { + _blockableOverlayState!.addMiddleBlockedListener((v) { if (!v) { isWindowFocus.value = false; if (isWindowFocus.value) { @@ -94,7 +94,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -129,16 +129,16 @@ class ChatModel with ChangeNotifier { showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); - final overlayState = pOverlayState?.getOverlayStateOrGlobal(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { return Listener( onPointerDown: (_) { if (!isWindowFocus.value) { isWindowFocus.value = true; - pOverlayState?.setMiddleBlocked(true); + _blockableOverlayState?.setMiddleBlocked(true); } }, child: DraggableChatWindow( @@ -154,7 +154,7 @@ class ChatModel with ChangeNotifier { hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { - pOverlayState?.setMiddleBlocked(false); + _blockableOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; From ac1ae9fc3bbfb7c7cd343222f59618957637093c Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 10:11:53 +0900 Subject: [PATCH 7/8] workaround: PageView reload --- .../lib/desktop/widgets/tabbar_widget.dart | 33 +++++++++++++++---- flutter/lib/models/model.dart | 4 +-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 598b2cc4c..ddc51eddb 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -327,14 +327,32 @@ class DesktopTab extends StatelessWidget { )); } + List _tabWidgets = []; Widget _buildPageView() { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), - children: state.value.tabs - .map((tab) => tab.page) - .toList(growable: false)))); + children: () { + /// to-do refactor, separate connection state and UI state for remote session. + /// [workaround] PageView children need an immutable list, after it has been passed into PageView + final tabLen = state.value.tabs.length; + if (tabLen == _tabWidgets.length) { + return _tabWidgets; + } else if (_tabWidgets.isNotEmpty && + tabLen == _tabWidgets.length + 1) { + /// On add. Use the previous list(pointer) to prevent item's state init twice. + /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built. + _tabWidgets.add(state.value.tabs.last.page); + return _tabWidgets; + } else { + /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal. + /// the Widgets in list must enable [AutomaticKeepAliveClientMixin] + final newList = state.value.tabs.map((v) => v.page).toList(); + _tabWidgets = newList; + return newList; + } + }()))); } /// Check whether to show ListView @@ -765,7 +783,8 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? MyTheme.tabbar(context).selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? + MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -1119,7 +1138,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, closeHoverColor: closeHoverColor ?? this.closeHoverColor, - selectedTabBackgroundColor: selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, + selectedTabBackgroundColor: + selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1145,7 +1165,8 @@ class TabbarTheme extends ThemeExtension { dividerColor: Color.lerp(dividerColor, other.dividerColor, t), hoverColor: Color.lerp(hoverColor, other.hoverColor, t), closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t), - selectedTabBackgroundColor: Color.lerp(selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), + selectedTabBackgroundColor: Color.lerp( + selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), ); } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 1eac1be39..5e4693ccc 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -17,7 +17,6 @@ import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/common/shared_state.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; import 'package:flutter_custom_cursor/cursor_manager.dart'; @@ -25,7 +24,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import '../common.dart'; -import '../common/shared_state.dart'; import '../utils/image.dart' as img; import '../mobile/widgets/dialog.dart'; import 'input_model.dart'; @@ -1348,13 +1346,13 @@ class FFI { canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } bind.sessionClose(id: id); - id = ''; imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); debugPrint('model $id closed'); + id = ''; } void setMethodCallHandler(FMethod callback) { From 552e45b320a6e1361580764332f8401801e7c160 Mon Sep 17 00:00:00 2001 From: csf Date: Wed, 8 Feb 2023 22:05:11 +0900 Subject: [PATCH 8/8] BlockableOverlay blocked layer transparent color --- flutter/lib/common/widgets/overlay.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 32dced02a..ba7b8a059 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -411,9 +411,8 @@ class BlockableOverlay extends StatelessWidget { state.onMiddleBlockedClick?.call(); }, child: Container( - color: state.middleBlocked.value - ? Colors.red.withOpacity(0.3) - : null)))), + color: + state.middleBlocked.value ? Colors.transparent : null)))), ]; if (upperLayer != null) {