mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-10-03 02:29:11 +00:00
fix(mobile): correct Chat keyboard and input helper behavior
- Keep soft keyboard and helper hidden when Chat opens via shortcut - Restore soft input on touch and remote focus after Chat closes - Handle modifier releases while Chat owns focus - Clear hidden virtual modifiers while preserving held keys and Pin behavior Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -41,6 +41,26 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
||||
}
|
||||
}
|
||||
|
||||
bool handleMobileChatKeyEvent(KeyEvent event,
|
||||
{required bool chatFocused, required InputModel inputModel}) {
|
||||
if (chatFocused &&
|
||||
event is KeyUpEvent &&
|
||||
const [
|
||||
PhysicalKeyboardKey.controlLeft,
|
||||
PhysicalKeyboardKey.controlRight,
|
||||
PhysicalKeyboardKey.altLeft,
|
||||
PhysicalKeyboardKey.altRight,
|
||||
PhysicalKeyboardKey.shiftLeft,
|
||||
PhysicalKeyboardKey.shiftRight,
|
||||
PhysicalKeyboardKey.metaLeft,
|
||||
PhysicalKeyboardKey.metaRight,
|
||||
].contains(event.physicalKey)) {
|
||||
// Chat owns focus, but the remote input path still owns these modifiers.
|
||||
inputModel.handleKeyEvent(event);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class RemotePage extends StatefulWidget {
|
||||
RemotePage(
|
||||
{Key? key,
|
||||
@@ -108,6 +128,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
});
|
||||
WakelockManager.enable(_uniqueKey);
|
||||
_physicalFocusNode.requestFocus();
|
||||
HardwareKeyboard.instance.addHandler(_handleChatKeyEvent);
|
||||
gFFI.inputModel.listenToMouse(true);
|
||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||
keyboardSubscription =
|
||||
@@ -133,6 +154,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
// Mobile has no DesktopTabController, so tab-switch shortcuts will
|
||||
// log a no-handler debug line if a user binds one.
|
||||
registerSessionShortcutActions(gFFI);
|
||||
_registerMobileChatShortcut();
|
||||
registerToolbarShortcuts(context, widget.id, gFFI);
|
||||
}
|
||||
});
|
||||
@@ -155,6 +177,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
HardwareKeyboard.instance.removeHandler(_handleChatKeyEvent);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
// Close the session up-front. `gFFI.close()` below only calls `sessionClose`
|
||||
// after several awaits (canvas save, image update, the `enable_soft_keyboard`
|
||||
@@ -266,10 +289,10 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
if (isIOS) {
|
||||
_iosKeyboardWorkaroundTimer?.cancel();
|
||||
_iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 100), () {
|
||||
if (!mounted) return;
|
||||
if (!mounted || gFFI.chatModel.inputNode.hasFocus) return;
|
||||
_physicalFocusNode.unfocus();
|
||||
_iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 50), () {
|
||||
if (!mounted) return;
|
||||
if (!mounted || gFFI.chatModel.inputNode.hasFocus) return;
|
||||
_physicalFocusNode.requestFocus();
|
||||
});
|
||||
});
|
||||
@@ -547,6 +570,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
);
|
||||
}
|
||||
|
||||
bool _handleChatKeyEvent(KeyEvent event) => handleMobileChatKeyEvent(event,
|
||||
chatFocused: gFFI.chatModel.inputNode.hasFocus, inputModel: inputModel);
|
||||
|
||||
Widget getRawPointerAndKeyBody(Widget child) {
|
||||
final ffiModel = Provider.of<FfiModel>(context);
|
||||
return RawPointerMouseRegion(
|
||||
@@ -668,7 +694,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
!gFFI.inputModel.relativeMouseMode.value;
|
||||
|
||||
Widget getBodyForMobile() {
|
||||
final keyboardIsVisible = keyboardVisibilityController.isVisible;
|
||||
return Container(
|
||||
color: MyTheme.canvasColor,
|
||||
child: Stack(children: () {
|
||||
@@ -679,9 +704,14 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
right: 10,
|
||||
child: QualityMonitor(gFFI.qualityMonitorModel),
|
||||
),
|
||||
KeyHelpTools(
|
||||
keyboardIsVisible: keyboardIsVisible,
|
||||
showGestureHelp: _showGestureHelp),
|
||||
Obx(() {
|
||||
final chatKeyboardRequested =
|
||||
gFFI.chatModel.softKeyboardRequested.value;
|
||||
return KeyHelpTools(
|
||||
keyboardIsVisible: keyboardVisibilityController.isVisible &&
|
||||
(_showEdit || chatKeyboardRequested),
|
||||
showGestureHelp: _showGestureHelp);
|
||||
}),
|
||||
SizedBox(
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -818,9 +848,18 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
||||
}();
|
||||
}
|
||||
|
||||
onPressedTextChat(String id) {
|
||||
void _registerMobileChatShortcut() {
|
||||
if (!isMobile) return;
|
||||
gFFI.shortcutModel.register(kShortcutActionToggleChat,
|
||||
() => onPressedTextChat(widget.id, requestSoftKeyboard: false));
|
||||
}
|
||||
|
||||
onPressedTextChat(String id, {bool requestSoftKeyboard = true}) {
|
||||
if (gFFI.chatModel.chatWindowOverlayEntry == null) {
|
||||
setState(() => _showEdit = false);
|
||||
}
|
||||
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
|
||||
gFFI.chatModel.toggleChatOverlay();
|
||||
gFFI.chatModel.toggleChatOverlay(requestSoftKeyboard: requestSoftKeyboard);
|
||||
}
|
||||
|
||||
showChatOptions(String id) async {
|
||||
@@ -951,6 +990,22 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
|
||||
InputModel get inputModel => gFFI.inputModel;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(KeyHelpTools oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (!_pin && oldWidget.requestShow && !widget.requestShow) {
|
||||
_releaseVirtualModifiers();
|
||||
}
|
||||
}
|
||||
|
||||
void _releaseVirtualModifiers() {
|
||||
final keyboard = HardwareKeyboard.instance;
|
||||
inputModel.ctrl = inputModel.ctrl && keyboard.isControlPressed;
|
||||
inputModel.alt = inputModel.alt && keyboard.isAltPressed;
|
||||
inputModel.shift = inputModel.shift && keyboard.isShiftPressed;
|
||||
inputModel.command = inputModel.command && keyboard.isMetaPressed;
|
||||
}
|
||||
|
||||
Widget wrap(String text, void Function() onPressed,
|
||||
{bool? active, IconData? icon}) {
|
||||
return TextButton(
|
||||
@@ -988,12 +1043,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasModifierOn = inputModel.ctrl ||
|
||||
inputModel.alt ||
|
||||
inputModel.shift ||
|
||||
inputModel.command;
|
||||
|
||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||
if (!_pin && !widget.requestShow) {
|
||||
gFFI.cursorModel
|
||||
.keyHelpToolsVisibilityChanged(null, widget.keyboardIsVisible);
|
||||
return Offstage();
|
||||
@@ -1032,9 +1082,10 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
||||
active: _fn),
|
||||
wrap(
|
||||
'',
|
||||
() => setState(
|
||||
() => _pin = !_pin,
|
||||
),
|
||||
() => setState(() {
|
||||
_pin = !_pin;
|
||||
if (!_pin && !widget.requestShow) _releaseVirtualModifiers();
|
||||
}),
|
||||
active: _pin,
|
||||
icon: Icons.push_pin),
|
||||
wrap(
|
||||
|
||||
@@ -64,8 +64,11 @@ class ChatModel with ChangeNotifier {
|
||||
RxBool isWindowFocus = true.obs;
|
||||
BlockableOverlayState _blockableOverlayState = BlockableOverlayState();
|
||||
final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
|
||||
final RxBool _softKeyboardRequested = false.obs;
|
||||
bool _usingPhysicalChatInput = false;
|
||||
|
||||
Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
|
||||
RxBool get softKeyboardRequested => _softKeyboardRequested;
|
||||
|
||||
TextEditingController textController = TextEditingController();
|
||||
RxInt mobileUnreadSum = 0.obs;
|
||||
@@ -80,6 +83,8 @@ class ChatModel with ChangeNotifier {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
inputNode.removeListener(_updateChatInputControl);
|
||||
_restoreChatInputControl();
|
||||
textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -194,13 +199,18 @@ class ChatModel with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
showChatWindowOverlay({Offset? chatInitPos}) {
|
||||
showChatWindowOverlay(
|
||||
{Offset? chatInitPos, bool requestSoftKeyboard = true}) {
|
||||
if (chatWindowOverlayEntry != null) return;
|
||||
isWindowFocus.value = true;
|
||||
_blockableOverlayState.setMiddleBlocked(true);
|
||||
|
||||
final overlayState = _blockableOverlayState.state;
|
||||
if (overlayState == null) return;
|
||||
if (isMobile) {
|
||||
_softKeyboardRequested.value = requestSoftKeyboard;
|
||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
}
|
||||
if (isMobile &&
|
||||
!gFFI.chatModel.currentKey.isOut && // not in remote page
|
||||
gFFI.chatModel.latestReceivedKey != null) {
|
||||
@@ -209,7 +219,8 @@ class ChatModel with ChangeNotifier {
|
||||
}
|
||||
final overlay = OverlayEntry(builder: (context) {
|
||||
return Listener(
|
||||
onPointerDown: (_) {
|
||||
onPointerDown: (event) {
|
||||
_requestChatSoftKeyboard(event);
|
||||
if (!isWindowFocus.value) {
|
||||
isWindowFocus.value = true;
|
||||
_blockableOverlayState.setMiddleBlocked(true);
|
||||
@@ -223,6 +234,10 @@ class ChatModel with ChangeNotifier {
|
||||
});
|
||||
overlayState.insert(overlay);
|
||||
chatWindowOverlayEntry = overlay;
|
||||
if (isMobile && !requestSoftKeyboard) {
|
||||
inputNode.addListener(_updateChatInputControl);
|
||||
_updateChatInputControl();
|
||||
}
|
||||
requestChatInputFocus();
|
||||
}
|
||||
|
||||
@@ -231,6 +246,9 @@ class ChatModel with ChangeNotifier {
|
||||
_blockableOverlayState.setMiddleBlocked(false);
|
||||
chatWindowOverlayEntry!.remove();
|
||||
chatWindowOverlayEntry = null;
|
||||
_softKeyboardRequested.value = false;
|
||||
inputNode.removeListener(_updateChatInputControl);
|
||||
_restoreChatInputControl();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -239,13 +257,14 @@ class ChatModel with ChangeNotifier {
|
||||
((!(isDesktop || isWebDesktop) && chatIconOverlayEntry == null) ||
|
||||
chatWindowOverlayEntry == null);
|
||||
|
||||
toggleChatOverlay({Offset? chatInitPos}) {
|
||||
toggleChatOverlay({Offset? chatInitPos, bool requestSoftKeyboard = true}) {
|
||||
if (_isChatOverlayHide()) {
|
||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
if (!isMobile) gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
if (!(isDesktop || isWebDesktop)) {
|
||||
showChatIconOverlay();
|
||||
}
|
||||
showChatWindowOverlay(chatInitPos: chatInitPos);
|
||||
showChatWindowOverlay(
|
||||
chatInitPos: chatInitPos, requestSoftKeyboard: requestSoftKeyboard);
|
||||
} else {
|
||||
hideChatIconOverlay();
|
||||
hideChatWindowOverlay();
|
||||
@@ -522,6 +541,47 @@ class ChatModel with ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
void _requestChatSoftKeyboard(PointerDownEvent event) {
|
||||
if (!isMobile || _softKeyboardRequested.value) return;
|
||||
inputNode.context?.visitAncestorElements((element) {
|
||||
if (element.widget is! TextField) return true;
|
||||
final box = element.findRenderObject();
|
||||
if (box is! RenderBox) return false;
|
||||
final bounds = box.localToGlobal(Offset.zero) & box.size;
|
||||
if (!bounds.contains(event.position)) return false;
|
||||
_softKeyboardRequested.value = true;
|
||||
_restoreChatInputControl();
|
||||
// Reconnect so the platform receives the keyboard type instead of none.
|
||||
inputNode.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild);
|
||||
scheduleMicrotask(() {
|
||||
if (chatWindowOverlayEntry != null && _softKeyboardRequested.value) {
|
||||
inputNode.requestFocus();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void _updateChatInputControl() {
|
||||
final physicalChatFocused = chatWindowOverlayEntry != null &&
|
||||
inputNode.hasFocus &&
|
||||
!_softKeyboardRequested.value;
|
||||
if (!physicalChatFocused) {
|
||||
_restoreChatInputControl();
|
||||
return;
|
||||
}
|
||||
if (_usingPhysicalChatInput) return;
|
||||
_usingPhysicalChatInput = true;
|
||||
// Keep the platform text-input connection for physical editing, without IME UI.
|
||||
TextInput.setInputControl(null);
|
||||
}
|
||||
|
||||
void _restoreChatInputControl() {
|
||||
if (!_usingPhysicalChatInput) return;
|
||||
_usingPhysicalChatInput = false;
|
||||
TextInput.restorePlatformInputControl();
|
||||
}
|
||||
|
||||
void onVoiceCallWaiting() {
|
||||
_voiceCallStatus.value = VoiceCallStatus.waitingForResponse;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user