diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 0d51cabdd..7e6e8ff86 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -228,49 +228,67 @@ class _RemotePageState extends State removeSharedStates(widget.id); } - Widget buildBody(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.background, - - /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay - /// see override build() in [BlockableOverlay] - body: BlockableOverlay( + Widget emptyOverlay() => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] state: _blockableOverlayState, 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(); - }); + color: Colors.transparent, + ), + ); + + Widget buildBody(BuildContext context) { + remoteToolbar(BuildContext context) => RemoteToolbar( + id: widget.id, + ffi: _ffi, + state: widget.toolbarState, + onEnterOrLeaveImageSetter: (func) => + _onEnterOrLeaveImage4Toolbar = func, + onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null, + ); + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Stack( + children: [ + 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); + } } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); - } - } - }, - inputModel: _ffi.inputModel, - child: getBodyForDesktop(context))), - upperLayer: [ - OverlayEntry( - builder: (context) => RemoteToolbar( - id: widget.id, - ffi: _ffi, - state: widget.toolbarState, - onEnterOrLeaveImageSetter: (func) => - _onEnterOrLeaveImage4Toolbar = func, - onEnterOrLeaveImageCleaner: () => - _onEnterOrLeaveImage4Toolbar = null, - )) + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + Obx(() => Stack( + children: [ + _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay() + : Offstage(), + // Use Overlay to enable rebuild every time on menu button click. + _ffi.ffiModel.pi.isSet.isTrue + ? Overlay(initialEntries: [ + OverlayEntry(builder: remoteToolbar) + ]) + : remoteToolbar(context), + _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), + ], + )), ], ), ); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 9c6ed6cec..4467adad4 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -266,7 +266,11 @@ class _ConnectionTabPageState extends State { if (e.kind != ui.PointerDeviceKind.mouse) { return; } - if (e.buttons == 2) { + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as RemotePage; + if (remotePage.ffi.ffiModel.pi.isSet.isTrue && + e.buttons == 2) { showRightMenu( (CancelFunc cancelFunc) { return _tabMenuBuilder(key, cancelFunc); diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 0eb59f51b..148135a81 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -77,7 +77,7 @@ CancelFunc showRightMenu(ToastBuilder builder, targetContext: context, verticalOffset: 0, horizontalOffset: 0, - duration: Duration(seconds: 4), + duration: Duration(seconds: 300), animationDuration: Duration(milliseconds: 0), animationReverseDuration: Duration(milliseconds: 0), preferDirection: PreferDirection.rightTop, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 202216b59..00b344e13 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -38,8 +38,6 @@ import 'platform_model.dart'; typedef HandleMsgBox = Function(Map evt, String id); typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool); -final _waitForImageDialogShow = {}; -final _waitForFirstImage = {}; final _constSessionId = Uuid().v4obj(); class CachedPeerData { @@ -100,6 +98,10 @@ class FfiModel with ChangeNotifier { WeakReference parent; late final SessionID sessionId; + RxBool waitForImageDialogShow = true.obs; + Timer? waitForImageTimer; + RxBool waitForFirstImage = true.obs; + Map get permissions => _permissions; Display get display => _display; @@ -158,6 +160,7 @@ class FfiModel with ChangeNotifier { _timer?.cancel(); _timer = null; clearPermissions(); + waitForImageTimer?.cancel(); } setConnectionType(String peerId, bool secure, bool direct) { @@ -498,7 +501,7 @@ class FfiModel with ChangeNotifier { closeConnection(); } - if (_waitForFirstImage[sessionId] == false) return; + if (waitForFirstImage.isFalse) return; dialogManager.show( (setState, close, context) => CustomAlertDialog( title: null, @@ -509,7 +512,12 @@ class FfiModel with ChangeNotifier { onCancel: onClose), tag: '$sessionId-waiting-for-image', ); - _waitForImageDialogShow[sessionId] = true; + waitForImageDialogShow.value = true; + waitForImageTimer = Timer(Duration(milliseconds: 1500), () { + if (waitForFirstImage.isTrue) { + bind.sessionInputOsPassword(sessionId: sessionId, value: ''); + } + }); bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId); } @@ -578,7 +586,7 @@ class FfiModel with ChangeNotifier { } if (displays.isNotEmpty) { _reconnects = 1; - _waitForFirstImage[sessionId] = true; + waitForFirstImage.value = true; } Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; @@ -602,6 +610,7 @@ class FfiModel with ChangeNotifier { } } + _pi.isSet.value = true; stateGlobal.resetLastResolutionGroupValues(peerId); notifyListeners(); @@ -1814,12 +1823,13 @@ class FFI { } void onEvent2UIRgba() async { - if (_waitForImageDialogShow[sessionId] == true) { - _waitForImageDialogShow[sessionId] = false; + if (ffiModel.waitForImageDialogShow.isTrue) { + ffiModel.waitForImageDialogShow.value = false; + ffiModel.waitForImageTimer?.cancel(); clearWaitingForImage(dialogManager, sessionId); } - if (_waitForFirstImage[sessionId] == true) { - _waitForFirstImage[sessionId] = false; + if (ffiModel.waitForFirstImage.value == true) { + ffiModel.waitForFirstImage.value = false; dialogManager.dismissAll(); await canvasModel.updateViewStyle(); await canvasModel.updateScrollStyle(); @@ -1934,7 +1944,7 @@ class Features { bool privacyMode = false; } -class PeerInfo { +class PeerInfo with ChangeNotifier { String version = ''; String username = ''; String hostname = ''; @@ -1946,6 +1956,8 @@ class PeerInfo { List resolutions = []; Map platform_additions = {}; + RxBool isSet = false.obs; + bool get is_wayland => platform_additions['is_wayland'] == true; bool get is_headless => platform_additions['headless'] == true; } diff --git a/src/client.rs b/src/client.rs index 9e3479da8..253c293ee 100644 --- a/src/client.rs +++ b/src/client.rs @@ -57,7 +57,10 @@ use scrap::{ ImageFormat, ImageRgb, }; -use crate::is_keyboard_mode_supported; +use crate::{ + common::input::{MOUSE_BUTTON_LEFT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, + is_keyboard_mode_supported, +}; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -2057,13 +2060,20 @@ pub fn send_pointer_device_event( /// # Arguments /// /// * `interface` - The interface for sending data. -fn activate_os(interface: &impl Interface) { +/// * `send_click` - Whether to send a click event. +fn activate_os(interface: &impl Interface, send_click: bool) { + let left_down = MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_DOWN; + let left_up = MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_UP; + send_mouse(left_up, 0, 0, false, false, false, false, interface); + std::thread::sleep(Duration::from_millis(50)); send_mouse(0, 0, 0, false, false, false, false, interface); std::thread::sleep(Duration::from_millis(50)); send_mouse(0, 3, 3, false, false, false, false, interface); - std::thread::sleep(Duration::from_millis(50)); - send_mouse(1 | 1 << 3, 0, 0, false, false, false, false, interface); - send_mouse(2 | 1 << 3, 0, 0, false, false, false, false, interface); + if send_click { + std::thread::sleep(Duration::from_millis(50)); + send_mouse(left_down, 0, 0, false, false, false, false, interface); + send_mouse(left_up, 0, 0, false, false, false, false, interface); + } /* let mut key_event = KeyEvent::new(); // do not use Esc, which has problem with Linux @@ -2096,10 +2106,15 @@ pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { /// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { + let input_password = !p.is_empty(); if activate { - activate_os(&interface); + // Click event is used to bring up the password input box. + activate_os(&interface, input_password); std::thread::sleep(Duration::from_millis(1200)); } + if !input_password { + return; + } let mut key_event = KeyEvent::new(); key_event.press = true; let mut msg_out = Message::new(); diff --git a/src/common.rs b/src/common.rs index 5ad92d914..36ca972b2 100644 --- a/src/common.rs +++ b/src/common.rs @@ -25,7 +25,7 @@ use hbb_common::{ protobuf::Enum, protobuf::Message as _, rendezvous_proto::*, - sleep, socket_client, + socket_client, tcp::FramedStream, tokio, ResultType, };