mirror of
https://github.com/rustdesk/rustdesk
synced 2026-04-21 13:27:19 +00:00
Merge 18fcfdb1fd into 803ac8cc4e
This commit is contained in:
commit
1706cf1a9a
5 changed files with 723 additions and 359 deletions
|
|
@ -164,6 +164,7 @@ const String kOptionVirtualMouseScale = "virtual-mouse-scale";
|
|||
const String kOptionShowVirtualJoystick = "show-virtual-joystick";
|
||||
const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note";
|
||||
const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys";
|
||||
const String kOptionAllowLegacyKeyMapping = "allow-legacy-key-mapping";
|
||||
|
||||
// network options
|
||||
const String kOptionAllowWebSocket = "allow-websocket";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
|
@ -43,10 +44,10 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) {
|
|||
class RemotePage extends StatefulWidget {
|
||||
RemotePage(
|
||||
{Key? key,
|
||||
required this.id,
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
required this.id,
|
||||
this.password,
|
||||
this.isSharedPassword,
|
||||
this.forceRelay})
|
||||
: super(key: key);
|
||||
|
||||
final String id;
|
||||
|
|
@ -64,7 +65,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
bool _showGestureHelp = false;
|
||||
String _value = '';
|
||||
Orientation? _currentOrientation;
|
||||
double _viewInsetsBottom = 0;
|
||||
final _uniqueKey = UniqueKey();
|
||||
Timer? _timerDidChangeMetrics;
|
||||
Timer? _iosKeyboardWorkaroundTimer;
|
||||
|
||||
final _blockableOverlayState = BlockableOverlayState();
|
||||
|
|
@ -79,7 +82,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
SessionID get sessionId => gFFI.sessionId;
|
||||
|
||||
final TextEditingController _textController =
|
||||
TextEditingController(text: initText);
|
||||
TextEditingController(text: initText);
|
||||
|
||||
_RemotePageState(String id) {
|
||||
initSharedStates(id);
|
||||
|
|
@ -137,6 +140,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
_physicalFocusNode.dispose();
|
||||
await gFFI.close();
|
||||
_timer?.cancel();
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
_iosKeyboardWorkaroundTimer?.cancel();
|
||||
gFFI.dialogManager.dismissAll();
|
||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||
|
|
@ -163,20 +167,46 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
gFFI.invokeMethod("try_sync_clipboard");
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeMetrics() {
|
||||
// If the soft keyboard is visible and the canvas has been changed(panned or scaled)
|
||||
// Don't try reset the view style and focus the cursor.
|
||||
if (gFFI.cursorModel.lastKeyboardIsVisible &&
|
||||
gFFI.canvasModel.isMobileCanvasChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
// safer fix: final newBottom = MediaQueryData.fromView(View.of(context)).viewInsets.bottom;
|
||||
// Given this repo is pinned to Flutter 3.24.5, ui.window may still be tolerated, but it is indeed deprecated in newer flutter versions.
|
||||
// Best fix in widget code is usually to stop using ui.window directly and use the active view from the framework side.
|
||||
// These kind of fixes should be made when more general flutter version update will happen but to not introduce
|
||||
// any regression current code will be kept intact
|
||||
final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom;
|
||||
_timerDidChangeMetrics?.cancel();
|
||||
_timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async {
|
||||
// We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`.
|
||||
if (newBottom != _viewInsetsBottom) {
|
||||
gFFI.canvasModel.mobileFocusCanvasCursor();
|
||||
_viewInsetsBottom = newBottom;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||
// But for now, the transparent color will cause the canvas to be white.
|
||||
// I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
|
||||
// But I don't know why and how to fix it.
|
||||
Widget emptyOverlay(Color bgColor) => BlockableOverlay(
|
||||
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
|
||||
/// see override build() in [BlockableOverlay]
|
||||
state: _blockableOverlayState,
|
||||
underlying: Container(
|
||||
color: bgColor,
|
||||
),
|
||||
);
|
||||
/// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
|
||||
/// see override build() in [BlockableOverlay]
|
||||
state: _blockableOverlayState,
|
||||
underlying: Container(
|
||||
color: bgColor,
|
||||
),
|
||||
);
|
||||
|
||||
void onSoftKeyboardChanged(bool visible) {
|
||||
inputModel.androidSoftKeyboardActive = visible;
|
||||
if (!visible) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
||||
// [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard
|
||||
|
|
@ -227,10 +257,10 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
// get common prefix of subNewValue and subOldValue
|
||||
var common = 0;
|
||||
for (;
|
||||
common < subOldValue.length &&
|
||||
common < subNewValue.length &&
|
||||
subNewValue[common] == subOldValue[common];
|
||||
++common) {}
|
||||
common < subOldValue.length &&
|
||||
common < subNewValue.length &&
|
||||
subNewValue[common] == subOldValue[common];
|
||||
++common) {}
|
||||
|
||||
// get newStr from subNewValue
|
||||
var newStr = "";
|
||||
|
|
@ -276,10 +306,20 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
if (newValue.length == oldValue.length) {
|
||||
// ?
|
||||
} else if (newValue.length < oldValue.length) {
|
||||
final char = 'VK_BACK';
|
||||
inputModel.inputKey(char);
|
||||
// Send exactly one VK_BACK per onChanged callback regardless of how many
|
||||
// characters the IME removed (Samsung accelerates held-delete). The
|
||||
// IME's own callback frequency provides a steady, controllable repeat
|
||||
// rate instead of runaway exponential deletion.
|
||||
inputModel.inputKey('VK_BACK');
|
||||
} else {
|
||||
final content = newValue.substring(oldValue.length);
|
||||
// Android IMEs like Gboard can leave modifier state (especially Shift)
|
||||
// logically active even though the inserted text is already composed.
|
||||
// Clear modifiers before forwarding soft-keyboard text so host-side
|
||||
// legacy character input does not apply Shift twice.
|
||||
inputModel.releaseTransientModifiersToHost();
|
||||
final composedContent =
|
||||
inputModel.shiftLocked ? content.toUpperCase() : content;
|
||||
if (content.length > 1) {
|
||||
if (oldValue != '' &&
|
||||
content.length == 2 &&
|
||||
|
|
@ -293,11 +333,11 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
content == '()' ||
|
||||
content == '【】')) {
|
||||
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
|
||||
bind.sessionInputString(sessionId: sessionId, value: content);
|
||||
bind.sessionInputString(sessionId: sessionId, value: composedContent);
|
||||
openKeyboard();
|
||||
return;
|
||||
}
|
||||
bind.sessionInputString(sessionId: sessionId, value: content);
|
||||
bind.sessionInputString(sessionId: sessionId, value: composedContent);
|
||||
} else {
|
||||
inputChar(content);
|
||||
}
|
||||
|
|
@ -314,12 +354,40 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
void inputChar(String char) {
|
||||
inputModel.releaseTransientModifiersToHost();
|
||||
final useLegacyKeyMapping =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowLegacyKeyMapping);
|
||||
final hasShortcutModifier = inputModel.ctrlLocked ||
|
||||
inputModel.altLocked ||
|
||||
inputModel.commandLocked;
|
||||
final shouldUseKeyPath = useLegacyKeyMapping || hasShortcutModifier;
|
||||
if (char == '\n') {
|
||||
char = 'VK_RETURN';
|
||||
if (shouldUseKeyPath) {
|
||||
char = 'VK_RETURN';
|
||||
} else {
|
||||
bind.sessionInputString(sessionId: inputModel.sessionId, value: '\n');
|
||||
return;
|
||||
}
|
||||
} else if (char == ' ') {
|
||||
char = 'VK_SPACE';
|
||||
if (shouldUseKeyPath) {
|
||||
char = 'VK_SPACE';
|
||||
} else {
|
||||
bind.sessionInputString(sessionId: inputModel.sessionId, value: ' ');
|
||||
return;
|
||||
}
|
||||
}
|
||||
inputModel.inputKey(char);
|
||||
if (!shouldUseKeyPath) {
|
||||
final value = inputModel.shiftLocked ? char.toUpperCase() : char;
|
||||
bind.sessionInputString(sessionId: inputModel.sessionId, value: value);
|
||||
return;
|
||||
}
|
||||
// Android soft-keyboard text is already composed; forwarding leaked modifier
|
||||
// state from IMEs like Gboard can make subsequent characters stay shifted.
|
||||
inputModel.inputKey(char,
|
||||
altOverride: inputModel.altLocked,
|
||||
ctrlOverride: inputModel.ctrlLocked,
|
||||
shiftOverride: inputModel.shiftLocked,
|
||||
commandOverride: inputModel.commandLocked);
|
||||
}
|
||||
|
||||
void openKeyboard() {
|
||||
|
|
@ -345,8 +413,8 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
Widget _bottomWidget() => _showGestureHelp
|
||||
? getGestureHelp()
|
||||
: (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
|
||||
? getBottomAppBar()
|
||||
: Offstage());
|
||||
? getBottomAppBar()
|
||||
: Offstage());
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -360,53 +428,53 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
// workaround for https://github.com/rustdesk/rustdesk/issues/3131
|
||||
// workaround for https://github.com/rustdesk/rustdesk/issues/3131
|
||||
floatingActionButtonLocation: keyboardIsVisible
|
||||
? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35)
|
||||
: null,
|
||||
floatingActionButton: !showActionButton
|
||||
? null
|
||||
: FloatingActionButton(
|
||||
mini: !keyboardIsVisible,
|
||||
child: Icon(
|
||||
(keyboardIsVisible || _showGestureHelp)
|
||||
? Icons.expand_more
|
||||
: Icons.expand_less,
|
||||
color: Colors.white,
|
||||
),
|
||||
backgroundColor: MyTheme.accent,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (keyboardIsVisible) {
|
||||
_showEdit = false;
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
_mobileFocusNode.unfocus();
|
||||
_physicalFocusNode.requestFocus();
|
||||
} else if (_showGestureHelp) {
|
||||
_showGestureHelp = false;
|
||||
} else {
|
||||
_showBar = !_showBar;
|
||||
}
|
||||
});
|
||||
}),
|
||||
mini: !keyboardIsVisible,
|
||||
child: Icon(
|
||||
(keyboardIsVisible || _showGestureHelp)
|
||||
? Icons.expand_more
|
||||
: Icons.expand_less,
|
||||
color: Colors.white,
|
||||
),
|
||||
backgroundColor: MyTheme.accent,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (keyboardIsVisible) {
|
||||
_showEdit = false;
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
_mobileFocusNode.unfocus();
|
||||
_physicalFocusNode.requestFocus();
|
||||
} else if (_showGestureHelp) {
|
||||
_showGestureHelp = false;
|
||||
} else {
|
||||
_showBar = !_showBar;
|
||||
}
|
||||
});
|
||||
}),
|
||||
bottomNavigationBar: Obx(() => Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
gFFI.ffiModel.pi.isSet.isTrue &&
|
||||
gFFI.ffiModel.waitForFirstImage.isTrue
|
||||
? emptyOverlay(MyTheme.canvasColor)
|
||||
: () {
|
||||
gFFI.ffiModel.tryShowAndroidActionsOverlay();
|
||||
return Offstage();
|
||||
}(),
|
||||
_bottomWidget(),
|
||||
gFFI.ffiModel.pi.isSet.isFalse
|
||||
? emptyOverlay(MyTheme.canvasColor)
|
||||
: Offstage(),
|
||||
],
|
||||
)),
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
gFFI.ffiModel.pi.isSet.isTrue &&
|
||||
gFFI.ffiModel.waitForFirstImage.isTrue
|
||||
? emptyOverlay(MyTheme.canvasColor)
|
||||
: () {
|
||||
gFFI.ffiModel.tryShowAndroidActionsOverlay();
|
||||
return Offstage();
|
||||
}(),
|
||||
_bottomWidget(),
|
||||
gFFI.ffiModel.pi.isSet.isFalse
|
||||
? emptyOverlay(MyTheme.canvasColor)
|
||||
: Offstage(),
|
||||
],
|
||||
)),
|
||||
body: Obx(
|
||||
() => getRawPointerAndKeyBody(Overlay(
|
||||
() => getRawPointerAndKeyBody(Overlay(
|
||||
initialEntries: [
|
||||
OverlayEntry(builder: (context) {
|
||||
return Container(
|
||||
|
|
@ -414,27 +482,27 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
child: isWebDesktop
|
||||
? getBodyForDesktopWithListener()
|
||||
: SafeArea(
|
||||
child:
|
||||
OrientationBuilder(builder: (ctx, orientation) {
|
||||
if (_currentOrientation != orientation) {
|
||||
Timer(const Duration(milliseconds: 200), () {
|
||||
gFFI.dialogManager
|
||||
.resetMobileActionsOverlay(ffi: gFFI);
|
||||
_currentOrientation = orientation;
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
});
|
||||
}
|
||||
return Container(
|
||||
color: MyTheme.canvasColor,
|
||||
child: inputModel.isPhysicalMouse.value
|
||||
? getBodyForMobile()
|
||||
: RawTouchGestureDetectorRegion(
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
),
|
||||
);
|
||||
}),
|
||||
child:
|
||||
OrientationBuilder(builder: (ctx, orientation) {
|
||||
if (_currentOrientation != orientation) {
|
||||
Timer(const Duration(milliseconds: 200), () {
|
||||
gFFI.dialogManager
|
||||
.resetMobileActionsOverlay(ffi: gFFI);
|
||||
_currentOrientation = orientation;
|
||||
gFFI.canvasModel.updateViewStyle();
|
||||
});
|
||||
}
|
||||
return Container(
|
||||
color: MyTheme.canvasColor,
|
||||
child: inputModel.isPhysicalMouse.value
|
||||
? getBodyForMobile()
|
||||
: RawTouchGestureDetectorRegion(
|
||||
child: getBodyForMobile(),
|
||||
ffi: gFFI,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
|
|
@ -452,9 +520,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
// The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog.
|
||||
child: gFFI.ffiModel.pi.isSet.isTrue
|
||||
? RawKeyFocusScope(
|
||||
focusNode: _physicalFocusNode,
|
||||
inputModel: inputModel,
|
||||
child: child)
|
||||
focusNode: _physicalFocusNode,
|
||||
inputModel: inputModel,
|
||||
child: child)
|
||||
: child,
|
||||
);
|
||||
}
|
||||
|
|
@ -470,70 +538,70 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.tv),
|
||||
onPressed: () {
|
||||
setState(() => _showEdit = false);
|
||||
showOptions(context, widget.id, gFFI.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
clientClose(sessionId, gFFI);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.tv),
|
||||
onPressed: () {
|
||||
setState(() => _showEdit = false);
|
||||
showOptions(context, widget.id, gFFI.dialogManager);
|
||||
},
|
||||
)
|
||||
] +
|
||||
(isWebDesktop || ffiModel.viewOnly || !ffiModel.keyboard
|
||||
? []
|
||||
: gFFI.ffiModel.isPeerAndroid
|
||||
? [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () => gFFI.dialogManager
|
||||
.toggleMobileActionsOverlay(ffi: gFFI),
|
||||
)
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(gFFI.ffiModel.touchMode
|
||||
? Icons.touch_app
|
||||
: Icons.mouse),
|
||||
onPressed: () => setState(
|
||||
() => _showGestureHelp = !_showGestureHelp),
|
||||
),
|
||||
]) +
|
||||
? [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: const Icon(Icons.build),
|
||||
onPressed: () => gFFI.dialogManager
|
||||
.toggleMobileActionsOverlay(ffi: gFFI),
|
||||
)
|
||||
]
|
||||
: [
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.keyboard),
|
||||
onPressed: openKeyboard),
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(gFFI.ffiModel.touchMode
|
||||
? Icons.touch_app
|
||||
: Icons.mouse),
|
||||
onPressed: () => setState(
|
||||
() => _showGestureHelp = !_showGestureHelp),
|
||||
),
|
||||
]) +
|
||||
(isWeb
|
||||
? []
|
||||
: <Widget>[
|
||||
futureBuilder(
|
||||
future: gFFI.invokeMethod(
|
||||
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
|
||||
hasData: (isSupportVoiceCall) => IconButton(
|
||||
color: Colors.white,
|
||||
icon: isAndroid && isSupportVoiceCall
|
||||
? SvgPicture.asset('assets/chat.svg',
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.white, BlendMode.srcIn))
|
||||
: Icon(Icons.message),
|
||||
onPressed: () =>
|
||||
isAndroid && isSupportVoiceCall
|
||||
? showChatOptions(widget.id)
|
||||
: onPressedTextChat(widget.id),
|
||||
))
|
||||
]) +
|
||||
futureBuilder(
|
||||
future: gFFI.invokeMethod(
|
||||
"get_value", "KEY_IS_SUPPORT_VOICE_CALL"),
|
||||
hasData: (isSupportVoiceCall) => IconButton(
|
||||
color: Colors.white,
|
||||
icon: isAndroid && isSupportVoiceCall
|
||||
? SvgPicture.asset('assets/chat.svg',
|
||||
colorFilter: ColorFilter.mode(
|
||||
Colors.white, BlendMode.srcIn))
|
||||
: Icon(Icons.message),
|
||||
onPressed: () =>
|
||||
isAndroid && isSupportVoiceCall
|
||||
? showChatOptions(widget.id)
|
||||
: onPressedTextChat(widget.id),
|
||||
))
|
||||
]) +
|
||||
[
|
||||
IconButton(
|
||||
color: Colors.white,
|
||||
|
|
@ -545,14 +613,14 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
),
|
||||
]),
|
||||
Obx(() => IconButton(
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.expand_more),
|
||||
onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
|
||||
? null
|
||||
: () {
|
||||
setState(() => _showBar = !_showBar);
|
||||
},
|
||||
)),
|
||||
color: Colors.white,
|
||||
icon: Icon(Icons.expand_more),
|
||||
onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
|
||||
? null
|
||||
: () {
|
||||
setState(() => _showBar = !_showBar);
|
||||
},
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
@ -560,8 +628,8 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
|
||||
bool get showCursorPaint =>
|
||||
!gFFI.ffiModel.isPeerAndroid &&
|
||||
!gFFI.canvasModel.cursorEmbedded &&
|
||||
!gFFI.inputModel.relativeMouseMode.value;
|
||||
!gFFI.canvasModel.cursorEmbedded &&
|
||||
!gFFI.inputModel.relativeMouseMode.value;
|
||||
|
||||
Widget getBodyForMobile() {
|
||||
final keyboardIsVisible = keyboardVisibilityController.isVisible;
|
||||
|
|
@ -584,29 +652,29 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
child: !_showEdit
|
||||
? Container()
|
||||
: TextFormField(
|
||||
textInputAction: TextInputAction.newline,
|
||||
autocorrect: false,
|
||||
// Flutter 3.16.9 Android.
|
||||
// `enableSuggestions` causes secure keyboard to be shown.
|
||||
// https://github.com/flutter/flutter/issues/139143
|
||||
// https://github.com/flutter/flutter/issues/146540
|
||||
// enableSuggestions: false,
|
||||
autofocus: true,
|
||||
focusNode: _mobileFocusNode,
|
||||
maxLines: null,
|
||||
controller: _textController,
|
||||
// trick way to make backspace work always
|
||||
keyboardType: TextInputType.multiline,
|
||||
// `onChanged` may be called depending on the input method if this widget is wrapped in
|
||||
// `Focus(onKeyEvent: ..., child: ...)`
|
||||
// For `Backspace` button in the soft keyboard:
|
||||
// en/fr input method:
|
||||
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
|
||||
// 2. The button will trigger `onKeyEvent` if the text field is empty.
|
||||
// ko/zh/ja input method: the button will trigger `onKeyEvent`
|
||||
// and the event will not popup if `KeyEventResult.handled` is returned.
|
||||
onChanged: handleSoftKeyboardInput,
|
||||
).workaroundFreezeLinuxMint(),
|
||||
textInputAction: TextInputAction.newline,
|
||||
autocorrect: false,
|
||||
// Flutter 3.16.9 Android.
|
||||
// `enableSuggestions` causes secure keyboard to be shown.
|
||||
// https://github.com/flutter/flutter/issues/139143
|
||||
// https://github.com/flutter/flutter/issues/146540
|
||||
// enableSuggestions: false,
|
||||
autofocus: true,
|
||||
focusNode: _mobileFocusNode,
|
||||
maxLines: null,
|
||||
controller: _textController,
|
||||
// trick way to make backspace work always
|
||||
keyboardType: TextInputType.multiline,
|
||||
// `onChanged` may be called depending on the input method if this widget is wrapped in
|
||||
// `Focus(onKeyEvent: ..., child: ...)`
|
||||
// For `Backspace` button in the soft keyboard:
|
||||
// en/fr input method:
|
||||
// 1. The button will not trigger `onKeyEvent` if the text field is not empty.
|
||||
// 2. The button will trigger `onKeyEvent` if the text field is empty.
|
||||
// ko/zh/ja input method: the button will trigger `onKeyEvent`
|
||||
// and the event will not popup if `KeyEventResult.handled` is returned.
|
||||
onChanged: handleSoftKeyboardInput,
|
||||
).workaroundFreezeLinuxMint(),
|
||||
),
|
||||
];
|
||||
if (showCursorPaint) {
|
||||
|
|
@ -686,15 +754,15 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
.asMap()
|
||||
.entries
|
||||
.map((e) =>
|
||||
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||
PopupMenuItem<int>(child: e.value.getChild(), value: e.key))
|
||||
.toList(),
|
||||
if (mobileActionMenus.isNotEmpty) PopupMenuDivider(),
|
||||
...menus
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => PopupMenuItem<int>(
|
||||
child: e.value.getChild(),
|
||||
value: e.key + mobileActionMenus.length))
|
||||
child: e.value.getChild(),
|
||||
value: e.key + mobileActionMenus.length))
|
||||
.toList(),
|
||||
];
|
||||
() async {
|
||||
|
|
@ -724,7 +792,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
|
||||
|
||||
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
|
||||
{TextStyle? labelStyle}) =>
|
||||
{TextStyle? labelStyle}) =>
|
||||
TTextMenu(
|
||||
child: Text(translate(label), style: labelStyle),
|
||||
trailingIcon: Transform.scale(
|
||||
|
|
@ -745,24 +813,24 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
].contains(gFFI.chatModel.voiceCallStatus.value);
|
||||
final menus = [
|
||||
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
|
||||
() => onPressedTextChat(widget.id)),
|
||||
() => onPressedTextChat(widget.id)),
|
||||
isInVoice
|
||||
? makeTextMenu(
|
||||
'End voice call',
|
||||
SvgPicture.asset(
|
||||
'assets/call_wait.svg',
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
|
||||
),
|
||||
onPressEndVoiceCall,
|
||||
labelStyle: TextStyle(color: Colors.redAccent))
|
||||
'End voice call',
|
||||
SvgPicture.asset(
|
||||
'assets/call_wait.svg',
|
||||
colorFilter:
|
||||
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
|
||||
),
|
||||
onPressEndVoiceCall,
|
||||
labelStyle: TextStyle(color: Colors.redAccent))
|
||||
: makeTextMenu(
|
||||
'Voice call',
|
||||
SvgPicture.asset(
|
||||
'assets/call_wait.svg',
|
||||
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
|
||||
),
|
||||
onPressVoiceCall),
|
||||
'Voice call',
|
||||
SvgPicture.asset(
|
||||
'assets/call_wait.svg',
|
||||
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
|
||||
),
|
||||
onPressVoiceCall),
|
||||
];
|
||||
|
||||
final menuItems = menus
|
||||
|
|
@ -804,24 +872,24 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
)));
|
||||
}
|
||||
|
||||
// * Currently mobile does not enable map mode
|
||||
// void changePhysicalKeyboardInputMode() async {
|
||||
// var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
|
||||
// gFFI.dialogManager.show((setState, close) {
|
||||
// void setMode(String? v) async {
|
||||
// await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? "");
|
||||
// setState(() => current = v ?? '');
|
||||
// Future.delayed(Duration(milliseconds: 300), close);
|
||||
// }
|
||||
//
|
||||
// return CustomAlertDialog(
|
||||
// title: Text(translate('Physical Keyboard Input Mode')),
|
||||
// content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// getRadio('Legacy mode', 'legacy', current, setMode),
|
||||
// getRadio('Map mode', 'map', current, setMode),
|
||||
// ]));
|
||||
// }, clickMaskDismiss: true);
|
||||
// }
|
||||
// * Currently mobile does not enable map mode
|
||||
// void changePhysicalKeyboardInputMode() async {
|
||||
// var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy";
|
||||
// gFFI.dialogManager.show((setState, close) {
|
||||
// void setMode(String? v) async {
|
||||
// await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? "");
|
||||
// setState(() => current = v ?? '');
|
||||
// Future.delayed(Duration(milliseconds: 300), close);
|
||||
// }
|
||||
//
|
||||
// return CustomAlertDialog(
|
||||
// title: Text(translate('Physical Keyboard Input Mode')),
|
||||
// content: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
// getRadio('Legacy mode', 'legacy', current, setMode),
|
||||
// getRadio('Map mode', 'map', current, setMode),
|
||||
// ]));
|
||||
// }, clickMaskDismiss: true);
|
||||
// }
|
||||
}
|
||||
|
||||
class KeyHelpTools extends StatefulWidget {
|
||||
|
|
@ -864,7 +932,7 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||
child: icon != null
|
||||
? Icon(icon, size: 14, color: Colors.white)
|
||||
: Text(translate(text),
|
||||
style: TextStyle(color: Colors.white, fontSize: 11)),
|
||||
style: TextStyle(color: Colors.white, fontSize: 11)),
|
||||
onPressed: onPressed);
|
||||
}
|
||||
|
||||
|
|
@ -887,7 +955,11 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||
final hasModifierOn = inputModel.ctrl ||
|
||||
inputModel.alt ||
|
||||
inputModel.shift ||
|
||||
inputModel.command;
|
||||
inputModel.command ||
|
||||
inputModel.ctrlLocked ||
|
||||
inputModel.altLocked ||
|
||||
inputModel.shiftLocked ||
|
||||
inputModel.commandLocked;
|
||||
|
||||
if (!_pin && !hasModifierOn && !widget.requestShow) {
|
||||
gFFI.cursorModel
|
||||
|
|
@ -902,47 +974,47 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||
final isLinux = pi.platform == kPeerPlatformLinux;
|
||||
final modifiers = <Widget>[
|
||||
wrap('Ctrl ', () {
|
||||
setState(() => inputModel.ctrl = !inputModel.ctrl);
|
||||
}, active: inputModel.ctrl),
|
||||
setState(() => inputModel.ctrlLocked = !inputModel.ctrlLocked);
|
||||
}, active: inputModel.ctrl || inputModel.ctrlLocked),
|
||||
wrap(' Alt ', () {
|
||||
setState(() => inputModel.alt = !inputModel.alt);
|
||||
}, active: inputModel.alt),
|
||||
setState(() => inputModel.altLocked = !inputModel.altLocked);
|
||||
}, active: inputModel.alt || inputModel.altLocked),
|
||||
wrap('Shift', () {
|
||||
setState(() => inputModel.shift = !inputModel.shift);
|
||||
}, active: inputModel.shift),
|
||||
setState(() => inputModel.shiftLocked = !inputModel.shiftLocked);
|
||||
}, active: inputModel.shift || inputModel.shiftLocked),
|
||||
wrap(isMac ? ' Cmd ' : ' Win ', () {
|
||||
setState(() => inputModel.command = !inputModel.command);
|
||||
}, active: inputModel.command),
|
||||
setState(() => inputModel.commandLocked = !inputModel.commandLocked);
|
||||
}, active: inputModel.command || inputModel.commandLocked),
|
||||
];
|
||||
final keys = <Widget>[
|
||||
wrap(
|
||||
' Fn ',
|
||||
() => setState(
|
||||
() => setState(
|
||||
() {
|
||||
_fn = !_fn;
|
||||
if (_fn) {
|
||||
_more = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
_fn = !_fn;
|
||||
if (_fn) {
|
||||
_more = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
active: _fn),
|
||||
wrap(
|
||||
'',
|
||||
() => setState(
|
||||
() => setState(
|
||||
() => _pin = !_pin,
|
||||
),
|
||||
),
|
||||
active: _pin,
|
||||
icon: Icons.push_pin),
|
||||
wrap(
|
||||
' ... ',
|
||||
() => setState(
|
||||
() => setState(
|
||||
() {
|
||||
_more = !_more;
|
||||
if (_more) {
|
||||
_fn = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
_more = !_more;
|
||||
if (_more) {
|
||||
_fn = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
active: _more),
|
||||
];
|
||||
final fn = <Widget>[
|
||||
|
|
@ -994,8 +1066,8 @@ class _KeyHelpToolsState extends State<KeyHelpTools> {
|
|||
inputModel.inputKey('VK_PAUSE');
|
||||
}),
|
||||
if (isWin || isLinux)
|
||||
// Maybe it's better to call it "Menu"
|
||||
// https://en.wikipedia.org/wiki/Menu_key
|
||||
// Maybe it's better to call it "Menu"
|
||||
// https://en.wikipedia.org/wiki/Menu_key
|
||||
wrap('Menu', () {
|
||||
inputModel.inputKey('Apps');
|
||||
}),
|
||||
|
|
@ -1139,7 +1211,7 @@ void showOptions(
|
|||
// - light theme: 0xff2196f3 (Colors.blue)
|
||||
// - dark theme: 0xff212121 (the canvas color?)
|
||||
final numBgSelected =
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.6);
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.6);
|
||||
for (var i = 0; i < pi.displays.length; ++i) {
|
||||
children.add(InkWell(
|
||||
onTap: () {
|
||||
|
|
@ -1158,7 +1230,7 @@ void showOptions(
|
|||
child: Text((i + 1).toString(),
|
||||
style: TextStyle(
|
||||
color:
|
||||
i == cur ? numColorSelected : numColorUnselected,
|
||||
i == cur ? numColorSelected : numColorUnselected,
|
||||
fontWeight: FontWeight.bold))))));
|
||||
}
|
||||
displays.add(Padding(
|
||||
|
|
@ -1174,13 +1246,13 @@ void showOptions(
|
|||
}
|
||||
|
||||
List<TRadioMenu<String>> viewStyleRadios =
|
||||
await toolbarViewStyle(context, id, gFFI);
|
||||
await toolbarViewStyle(context, id, gFFI);
|
||||
List<TRadioMenu<String>> imageQualityRadios =
|
||||
await toolbarImageQuality(context, id, gFFI);
|
||||
await toolbarImageQuality(context, id, gFFI);
|
||||
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
||||
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
||||
List<TToggleMenu> displayToggles =
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
await toolbarDisplayToggle(context, id, gFFI);
|
||||
|
||||
List<TToggleMenu> privacyModeList = [];
|
||||
// privacy mode
|
||||
|
|
@ -1207,9 +1279,9 @@ void showOptions(
|
|||
viewStyle.value,
|
||||
e.onChanged != null
|
||||
? (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) viewStyle.value = v;
|
||||
}
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) viewStyle.value = v;
|
||||
}
|
||||
: null)),
|
||||
// Show custom scale controls when custom view style is selected
|
||||
Obx(() => viewStyle.value == kRemoteViewStyleCustom
|
||||
|
|
@ -1223,9 +1295,9 @@ void showOptions(
|
|||
imageQuality.value,
|
||||
e.onChanged != null
|
||||
? (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) imageQuality.value = v;
|
||||
}
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) imageQuality.value = v;
|
||||
}
|
||||
: null)),
|
||||
const Divider(color: MyTheme.border),
|
||||
for (var e in codecRadios)
|
||||
|
|
@ -1235,9 +1307,9 @@ void showOptions(
|
|||
codec.value,
|
||||
e.onChanged != null
|
||||
? (v) {
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) codec.value = v;
|
||||
}
|
||||
e.onChanged?.call(v);
|
||||
if (v != null) codec.value = v;
|
||||
}
|
||||
: null)),
|
||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||
];
|
||||
|
|
@ -1246,16 +1318,16 @@ void showOptions(
|
|||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxCursorToggleValues[e.key].value,
|
||||
onChanged: e.value.onChanged != null
|
||||
? (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||
}
|
||||
: null,
|
||||
title: e.value.child)))
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxCursorToggleValues[e.key].value,
|
||||
onChanged: e.value.onChanged != null
|
||||
? (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||
}
|
||||
: null,
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
|
||||
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
||||
|
|
@ -1263,16 +1335,16 @@ void showOptions(
|
|||
.asMap()
|
||||
.entries
|
||||
.map((e) => Obx(() => CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxToggleValues[e.key].value,
|
||||
onChanged: e.value.onChanged != null
|
||||
? (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxToggleValues[e.key].value = v;
|
||||
}
|
||||
: null,
|
||||
title: e.value.child)))
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
value: rxToggleValues[e.key].value,
|
||||
onChanged: e.value.onChanged != null
|
||||
? (v) {
|
||||
e.value.onChanged?.call(v);
|
||||
if (v != null) rxToggleValues[e.key].value = v;
|
||||
}
|
||||
: null,
|
||||
title: e.value.child)))
|
||||
.toList();
|
||||
final toggles = [
|
||||
...cursorTogglesList,
|
||||
|
|
@ -1373,19 +1445,19 @@ TTextMenu? getResolutionMenu(FFI ffi, String id) {
|
|||
ffi.dialogManager.show((setState, close, context) {
|
||||
final children = resolutions
|
||||
.map((e) => getRadio<String>(
|
||||
Text('${e.width}x${e.height}'),
|
||||
'${e.width}x${e.height}',
|
||||
'${display.width}x${display.height}',
|
||||
(value) {
|
||||
close();
|
||||
bind.sessionChangeResolution(
|
||||
sessionId: ffi.sessionId,
|
||||
display: pi.currentDisplay,
|
||||
width: e.width,
|
||||
height: e.height,
|
||||
);
|
||||
},
|
||||
))
|
||||
Text('${e.width}x${e.height}'),
|
||||
'${e.width}x${e.height}',
|
||||
'${display.width}x${display.height}',
|
||||
(value) {
|
||||
close();
|
||||
bind.sessionChangeResolution(
|
||||
sessionId: ffi.sessionId,
|
||||
display: pi.currentDisplay,
|
||||
width: e.width,
|
||||
height: e.height,
|
||||
);
|
||||
},
|
||||
))
|
||||
.toList();
|
||||
return CustomAlertDialog(
|
||||
title: Text(translate('Resolution')),
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||
var _isUsingPublicServer = false;
|
||||
var _allowAskForNoteAtEndOfConnection = false;
|
||||
var _preventSleepWhileConnected = true;
|
||||
var _useLegacyKeyMapping = false;
|
||||
|
||||
_SettingsState() {
|
||||
_enableAbr = option2bool(
|
||||
|
|
@ -145,6 +146,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||
mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions);
|
||||
_showTerminalExtraKeys =
|
||||
mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys);
|
||||
_useLegacyKeyMapping =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowLegacyKeyMapping);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -934,6 +937,24 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||
tiles: shareScreenTiles,
|
||||
),
|
||||
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||
if (isAndroid && !incomingOnly)
|
||||
SettingsSection(
|
||||
title: Text(translate('Keyboard input')),
|
||||
tiles: [
|
||||
SettingsTile.switchTile(
|
||||
title: Text(translate('Use Legacy Key Mapping')),
|
||||
initialValue: _useLegacyKeyMapping,
|
||||
onToggle: (v) async {
|
||||
await mainSetLocalBoolOption(kOptionAllowLegacyKeyMapping, v);
|
||||
final newValue =
|
||||
mainGetLocalBoolOptionSync(kOptionAllowLegacyKeyMapping);
|
||||
setState(() {
|
||||
_useLegacyKeyMapping = newValue;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isAndroid &&
|
||||
!disabledSettings &&
|
||||
!outgoingOnly &&
|
||||
|
|
|
|||
|
|
@ -335,6 +335,19 @@ class InputModel {
|
|||
var ctrl = false;
|
||||
var alt = false;
|
||||
var command = false;
|
||||
var shiftLocked = false;
|
||||
var ctrlLocked = false;
|
||||
var altLocked = false;
|
||||
var commandLocked = false;
|
||||
var hardwareShiftLeft = false;
|
||||
var hardwareShiftRight = false;
|
||||
var hardwareCtrlLeft = false;
|
||||
var hardwareCtrlRight = false;
|
||||
var hardwareAltLeft = false;
|
||||
var hardwareAltRight = false;
|
||||
var hardwareCommandLeft = false;
|
||||
var hardwareCommandRight = false;
|
||||
var hardwareCommandSuper = false;
|
||||
|
||||
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
|
||||
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
||||
|
|
@ -367,6 +380,12 @@ class InputModel {
|
|||
bool _pointerMovedAfterEnter = false;
|
||||
bool _pointerInsideImage = false;
|
||||
|
||||
/// True while the Android soft keyboard editor is active.
|
||||
/// When set, key events are ignored so they flow through to the
|
||||
/// hidden TextFormField's onChanged handler instead of being
|
||||
/// processed here with potentially incorrect physicalKey data.
|
||||
bool androidSoftKeyboardActive = false;
|
||||
|
||||
// mouse
|
||||
final isPhysicalMouse = false.obs;
|
||||
int _lastButtons = 0;
|
||||
|
|
@ -377,7 +396,7 @@ class InputModel {
|
|||
static const int _wheelAccelFastThresholdUs = 40000; // 40ms
|
||||
static const int _wheelAccelMediumThresholdUs = 80000; // 80ms
|
||||
static const double _wheelBurstVelocityThreshold =
|
||||
0.002; // delta units per microsecond
|
||||
0.002; // delta units per microsecond
|
||||
// Wheel burst acceleration (empirical tuning).
|
||||
// Applies only to fast, non-smooth bursts to preserve single-step scrolling.
|
||||
// Flutter uses microseconds for dt, so velocity is in delta/us.
|
||||
|
|
@ -526,8 +545,8 @@ class InputModel {
|
|||
/// - Default: `kDefaultTrackpadSpeed`
|
||||
Future<void> updateTrackpadSpeed() async {
|
||||
_trackpadSpeed =
|
||||
(await bind.sessionGetTrackpadSpeed(sessionId: sessionId) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
(await bind.sessionGetTrackpadSpeed(sessionId: sessionId) ??
|
||||
kDefaultTrackpadSpeed);
|
||||
if (_trackpadSpeed < kMinTrackpadSpeed ||
|
||||
_trackpadSpeed > kMaxTrackpadSpeed) {
|
||||
_trackpadSpeed = kDefaultTrackpadSpeed;
|
||||
|
|
@ -537,10 +556,15 @@ class InputModel {
|
|||
|
||||
void handleKeyDownEventModifiers(KeyEvent e) {
|
||||
KeyUpEvent upEvent(e) => KeyUpEvent(
|
||||
physicalKey: e.physicalKey,
|
||||
logicalKey: e.logicalKey,
|
||||
timeStamp: e.timeStamp,
|
||||
);
|
||||
physicalKey: e.physicalKey,
|
||||
logicalKey: e.logicalKey,
|
||||
timeStamp: e.timeStamp,
|
||||
);
|
||||
if (!(isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
_isAndroidSoftKeyboardEvent(e))) {
|
||||
_setHardwareModifierState(e.logicalKey, true);
|
||||
}
|
||||
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
||||
if (!alt) {
|
||||
alt = true;
|
||||
|
|
@ -590,6 +614,11 @@ class InputModel {
|
|||
}
|
||||
|
||||
void handleKeyUpEventModifiers(KeyEvent e) {
|
||||
if (!(isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
_isAndroidSoftKeyboardEvent(e))) {
|
||||
_setHardwareModifierState(e.logicalKey, false);
|
||||
}
|
||||
if (e.logicalKey == LogicalKeyboardKey.altLeft) {
|
||||
alt = false;
|
||||
toReleaseKeys.lastLAltKeyEvent = null;
|
||||
|
|
@ -620,9 +649,79 @@ class InputModel {
|
|||
}
|
||||
}
|
||||
|
||||
bool _isSoftKeyboardPhysicalKey(PhysicalKeyboardKey key) {
|
||||
final usbHidUsage = key.usbHidUsage;
|
||||
// Flutter hardware keyboard HID usages are normally in the low keyboard-page
|
||||
// range (for example 0x00070004 for 'A', 0x00070028 for Enter), which means
|
||||
// bits above the lower 20-bit region are zero and `usbHidUsage >> 20` becomes
|
||||
// 0. Some Android IME/soft-keyboard events instead report abnormal high-bit
|
||||
// values (observed examples look like 0x1100000042), so shifting right by 20
|
||||
// leaves a non-zero value. We use that cheap heuristic here to treat such
|
||||
// events as soft-keyboard/IME-generated rather than real hardware keys.
|
||||
final isNormalUsbHidUsage = (usbHidUsage >> 20) == 0;
|
||||
// Real hardware keyboard events generally have a normal keyboard HID usage.
|
||||
// IME/soft-keyboard events on Android often do not, especially for modifier
|
||||
// and special keys when the on-screen keyboard is visible.
|
||||
return !isNormalUsbHidUsage;
|
||||
}
|
||||
|
||||
bool _isAndroidSoftKeyboardEvent(KeyEvent e) {
|
||||
return _isSoftKeyboardPhysicalKey(e.physicalKey);
|
||||
}
|
||||
|
||||
bool _isModifierLogicalKey(LogicalKeyboardKey key) {
|
||||
return key == LogicalKeyboardKey.shiftLeft ||
|
||||
key == LogicalKeyboardKey.shiftRight ||
|
||||
key == LogicalKeyboardKey.controlLeft ||
|
||||
key == LogicalKeyboardKey.controlRight ||
|
||||
key == LogicalKeyboardKey.altLeft ||
|
||||
key == LogicalKeyboardKey.altRight ||
|
||||
key == LogicalKeyboardKey.metaLeft ||
|
||||
key == LogicalKeyboardKey.metaRight ||
|
||||
key == LogicalKeyboardKey.superKey;
|
||||
}
|
||||
|
||||
bool _isAndroidSoftKeyboardRawEvent(RawKeyEvent e) {
|
||||
return _isSoftKeyboardPhysicalKey(e.physicalKey);
|
||||
}
|
||||
|
||||
void _setHardwareModifierState(LogicalKeyboardKey key, bool down) {
|
||||
if (key == LogicalKeyboardKey.shiftLeft) {
|
||||
hardwareShiftLeft = down;
|
||||
} else if (key == LogicalKeyboardKey.shiftRight) {
|
||||
hardwareShiftRight = down;
|
||||
} else if (key == LogicalKeyboardKey.controlLeft) {
|
||||
hardwareCtrlLeft = down;
|
||||
} else if (key == LogicalKeyboardKey.controlRight) {
|
||||
hardwareCtrlRight = down;
|
||||
} else if (key == LogicalKeyboardKey.altLeft) {
|
||||
hardwareAltLeft = down;
|
||||
} else if (key == LogicalKeyboardKey.altRight) {
|
||||
hardwareAltRight = down;
|
||||
} else if (key == LogicalKeyboardKey.metaLeft) {
|
||||
hardwareCommandLeft = down;
|
||||
} else if (key == LogicalKeyboardKey.metaRight) {
|
||||
hardwareCommandRight = down;
|
||||
} else if (key == LogicalKeyboardKey.superKey) {
|
||||
hardwareCommandSuper = down;
|
||||
}
|
||||
}
|
||||
|
||||
KeyEventResult handleRawKeyEvent(RawKeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if (isViewCamera) return KeyEventResult.handled;
|
||||
final isAndroidSoftRaw = isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
_isAndroidSoftKeyboardRawEvent(e);
|
||||
if (isAndroidSoftRaw &&
|
||||
!_hasHardwareModifierPressed &&
|
||||
_isModifierLogicalKey(e.logicalKey)) {
|
||||
alt = false;
|
||||
ctrl = false;
|
||||
shift = false;
|
||||
command = false;
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
|
|
@ -642,6 +741,9 @@ class InputModel {
|
|||
|
||||
final key = e.logicalKey;
|
||||
if (e is RawKeyDownEvent) {
|
||||
if (!isAndroidSoftRaw) {
|
||||
_setHardwareModifierState(key, true);
|
||||
}
|
||||
if (!e.repeat) {
|
||||
if (e.isAltPressed && !alt) {
|
||||
alt = true;
|
||||
|
|
@ -656,6 +758,9 @@ class InputModel {
|
|||
toReleaseRawKeys.updateKeyDown(key, e);
|
||||
}
|
||||
if (e is RawKeyUpEvent) {
|
||||
if (!isAndroidSoftRaw) {
|
||||
_setHardwareModifierState(key, false);
|
||||
}
|
||||
if (key == LogicalKeyboardKey.altLeft ||
|
||||
key == LogicalKeyboardKey.altRight) {
|
||||
alt = false;
|
||||
|
|
@ -687,6 +792,39 @@ class InputModel {
|
|||
KeyEventResult handleKeyEvent(KeyEvent e) {
|
||||
if (isViewOnly) return KeyEventResult.handled;
|
||||
if (isViewCamera) return KeyEventResult.handled;
|
||||
// When the Android soft keyboard is active, avoid processing key events
|
||||
// through the normal input pipeline because physicalKey data from the
|
||||
// soft keyboard is unreliable (Flutter issue #157771) and can corrupt
|
||||
// subsequent input, causing every keypress to repeat a single character.
|
||||
//
|
||||
// Return `handled` (not `ignored`) so Android keeps sending key-repeat
|
||||
// events for held keys and the TextFormField does not consume sentinel
|
||||
// buffer characters.
|
||||
//
|
||||
// For Backspace and Enter, send them directly using the reliable logical
|
||||
// key data. This is required because for some IMEs (ko/zh/ja) returning
|
||||
// `handled` prevents the IME from processing the key through onChanged.
|
||||
if (isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
!_hasHardwareModifierPressed &&
|
||||
_isModifierLogicalKey(e.logicalKey)) {
|
||||
releaseTransientModifiersToHost();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
_isAndroidSoftKeyboardEvent(e)) {
|
||||
if (e is KeyDownEvent || e is KeyRepeatEvent) {
|
||||
if (e.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
inputKey('VK_BACK', press: true);
|
||||
} else if (e.logicalKey == LogicalKeyboardKey.enter ||
|
||||
e.logicalKey == LogicalKeyboardKey.numpadEnter) {
|
||||
inputKey('VK_RETURN', press: true);
|
||||
}
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
if (!isInputSourceFlutter) {
|
||||
if (isDesktop) {
|
||||
return KeyEventResult.handled;
|
||||
|
|
@ -873,7 +1011,13 @@ class InputModel {
|
|||
/// Send key stroke event.
|
||||
/// [down] indicates the key's state(down or up).
|
||||
/// [press] indicates a click event(down and up).
|
||||
void inputKey(String name, {bool? down, bool? press}) {
|
||||
void inputKey(String name,
|
||||
{bool? down,
|
||||
bool? press,
|
||||
bool? altOverride,
|
||||
bool? ctrlOverride,
|
||||
bool? shiftOverride,
|
||||
bool? commandOverride}) {
|
||||
if (!keyboardPerm) return;
|
||||
if (isViewCamera) return;
|
||||
bind.sessionInputKey(
|
||||
|
|
@ -881,16 +1025,16 @@ class InputModel {
|
|||
name: name,
|
||||
down: down ?? false,
|
||||
press: press ?? true,
|
||||
alt: alt,
|
||||
ctrl: ctrl,
|
||||
shift: shift,
|
||||
command: command);
|
||||
alt: altOverride ?? (alt || altLocked),
|
||||
ctrl: ctrlOverride ?? (ctrl || ctrlLocked),
|
||||
shift: shiftOverride ?? (shift || shiftLocked),
|
||||
command: commandOverride ?? (command || commandLocked));
|
||||
}
|
||||
|
||||
static Map<String, dynamic> getMouseEventMove() => {
|
||||
'type': _kMouseEventMove,
|
||||
'buttons': 0,
|
||||
};
|
||||
'type': _kMouseEventMove,
|
||||
'buttons': 0,
|
||||
};
|
||||
|
||||
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
|
||||
final Map<String, dynamic> out = {};
|
||||
|
|
@ -946,6 +1090,11 @@ class InputModel {
|
|||
/// Send scroll event with scroll distance [y].
|
||||
Future<void> scroll(int y) async {
|
||||
if (isViewCamera) return;
|
||||
if (isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
!_hasHardwareModifierPressed) {
|
||||
releaseTransientModifiersToHost();
|
||||
}
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json
|
||||
|
|
@ -955,14 +1104,92 @@ class InputModel {
|
|||
/// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command].
|
||||
void resetModifiers() {
|
||||
shift = ctrl = alt = command = false;
|
||||
shiftLocked = ctrlLocked = altLocked = commandLocked = false;
|
||||
hardwareShiftLeft = hardwareShiftRight = false;
|
||||
hardwareCtrlLeft = hardwareCtrlRight = false;
|
||||
hardwareAltLeft = hardwareAltRight = false;
|
||||
hardwareCommandLeft = hardwareCommandRight = hardwareCommandSuper = false;
|
||||
}
|
||||
|
||||
void releaseTransientModifiersToHost() {
|
||||
if (shift && !shiftLocked) {
|
||||
inputKey('VK_SHIFT',
|
||||
down: false,
|
||||
press: false,
|
||||
altOverride: false,
|
||||
ctrlOverride: false,
|
||||
shiftOverride: false,
|
||||
commandOverride: false);
|
||||
}
|
||||
if (ctrl && !ctrlLocked) {
|
||||
inputKey('VK_CONTROL',
|
||||
down: false,
|
||||
press: false,
|
||||
altOverride: false,
|
||||
ctrlOverride: false,
|
||||
shiftOverride: false,
|
||||
commandOverride: false);
|
||||
}
|
||||
if (alt && !altLocked) {
|
||||
inputKey('VK_MENU',
|
||||
down: false,
|
||||
press: false,
|
||||
altOverride: false,
|
||||
ctrlOverride: false,
|
||||
shiftOverride: false,
|
||||
commandOverride: false);
|
||||
}
|
||||
if (command && !commandLocked) {
|
||||
inputKey('Meta',
|
||||
down: false,
|
||||
press: false,
|
||||
altOverride: false,
|
||||
ctrlOverride: false,
|
||||
shiftOverride: false,
|
||||
commandOverride: false);
|
||||
}
|
||||
shift = false;
|
||||
ctrl = false;
|
||||
alt = false;
|
||||
command = false;
|
||||
}
|
||||
|
||||
bool get _hasHardwareModifierPressed {
|
||||
return _hardwareShiftPressed ||
|
||||
_hardwareCtrlPressed ||
|
||||
_hardwareAltPressed ||
|
||||
_hardwareCommandPressed;
|
||||
}
|
||||
|
||||
bool get _hardwareShiftPressed {
|
||||
return hardwareShiftLeft || hardwareShiftRight;
|
||||
}
|
||||
|
||||
bool get _hardwareCtrlPressed {
|
||||
return hardwareCtrlLeft || hardwareCtrlRight;
|
||||
}
|
||||
|
||||
bool get _hardwareAltPressed {
|
||||
return hardwareAltLeft || hardwareAltRight;
|
||||
}
|
||||
|
||||
bool get _hardwareCommandPressed {
|
||||
return hardwareCommandLeft || hardwareCommandRight || hardwareCommandSuper;
|
||||
}
|
||||
|
||||
/// Modify the given modifier map [evt] based on current modifier key status.
|
||||
Map<String, dynamic> modify(Map<String, dynamic> evt) {
|
||||
if (ctrl) evt['ctrl'] = 'true';
|
||||
if (shift) evt['shift'] = 'true';
|
||||
if (alt) evt['alt'] = 'true';
|
||||
if (command) evt['command'] = 'true';
|
||||
if (isAndroid) {
|
||||
if (_hardwareCtrlPressed || ctrlLocked) evt['ctrl'] = 'true';
|
||||
if (_hardwareShiftPressed || shiftLocked) evt['shift'] = 'true';
|
||||
if (_hardwareAltPressed || altLocked) evt['alt'] = 'true';
|
||||
if (_hardwareCommandPressed || commandLocked) evt['command'] = 'true';
|
||||
return evt;
|
||||
}
|
||||
if (ctrl || ctrlLocked) evt['ctrl'] = 'true';
|
||||
if (shift || shiftLocked) evt['shift'] = 'true';
|
||||
if (alt || altLocked) evt['alt'] = 'true';
|
||||
if (command || commandLocked) evt['command'] = 'true';
|
||||
return evt;
|
||||
}
|
||||
|
||||
|
|
@ -970,6 +1197,11 @@ class InputModel {
|
|||
Future<void> sendMouse(String type, MouseButtons button) async {
|
||||
if (!keyboardPerm) return;
|
||||
if (isViewCamera) return;
|
||||
if (isAndroid &&
|
||||
androidSoftKeyboardActive &&
|
||||
!_hasHardwareModifierPressed) {
|
||||
releaseTransientModifiersToHost();
|
||||
}
|
||||
await bind.sessionSendMouse(
|
||||
sessionId: sessionId,
|
||||
msg: json.encode(modify({'type': type, 'buttons': button.value})));
|
||||
|
|
@ -1417,7 +1649,7 @@ class InputModel {
|
|||
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
|
||||
List<RemoteWindowCoords> remoteWindowCoords) async {
|
||||
final coords =
|
||||
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
|
||||
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
|
||||
final wc = WindowController.fromWindowId(kWindowId!);
|
||||
try {
|
||||
final frame = await wc.getFrame();
|
||||
|
|
@ -1445,7 +1677,8 @@ class InputModel {
|
|||
if (e is PointerScrollEvent) {
|
||||
final rawDx = e.scrollDelta.dx;
|
||||
final rawDy = e.scrollDelta.dy;
|
||||
final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
|
||||
final dominantDelta =
|
||||
rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs();
|
||||
final isSmooth = dominantDelta < 1;
|
||||
final nowUs = DateTime.now().microsecondsSinceEpoch;
|
||||
final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs;
|
||||
|
|
@ -1489,18 +1722,18 @@ class InputModel {
|
|||
}
|
||||
|
||||
void refreshMousePos() => handleMouse({
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
}, lastMousePos, edgeScroll: useEdgeScroll);
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
}, lastMousePos, edgeScroll: useEdgeScroll);
|
||||
|
||||
void tryMoveEdgeOnExit(Offset pos) => handleMouse(
|
||||
{
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
},
|
||||
pos,
|
||||
onExit: true,
|
||||
);
|
||||
{
|
||||
'buttons': 0,
|
||||
'type': _kMouseEventMove,
|
||||
},
|
||||
pos,
|
||||
onExit: true,
|
||||
);
|
||||
|
||||
static double tryGetNearestRange(double v, double min, double max, double n) {
|
||||
if (v < min && v >= min - n) {
|
||||
|
|
@ -1594,12 +1827,12 @@ class InputModel {
|
|||
}
|
||||
|
||||
Map<String, dynamic>? processEventToPeer(
|
||||
Map<String, dynamic> evt,
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
Map<String, dynamic> evt,
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
if (isViewCamera) return null;
|
||||
double x = offset.dx;
|
||||
double y = max(0.0, offset.dy);
|
||||
|
|
@ -1669,12 +1902,12 @@ class InputModel {
|
|||
}
|
||||
|
||||
Map<String, dynamic>? handleMouse(
|
||||
Map<String, dynamic> evt,
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
Map<String, dynamic> evt,
|
||||
Offset offset, {
|
||||
bool onExit = false,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final evtToPeer = processEventToPeer(evt, offset,
|
||||
onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll);
|
||||
if (evtToPeer != null) {
|
||||
|
|
@ -1685,19 +1918,19 @@ class InputModel {
|
|||
}
|
||||
|
||||
Point? handlePointerDevicePos(
|
||||
String kind,
|
||||
double x,
|
||||
double y,
|
||||
bool isMove,
|
||||
String evtType, {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
String kind,
|
||||
double x,
|
||||
double y,
|
||||
bool isMove,
|
||||
String evtType, {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
bool moveCanvas = true,
|
||||
bool edgeScroll = false,
|
||||
}) {
|
||||
final ffiModel = parent.target!.ffiModel;
|
||||
CanvasCoords canvas =
|
||||
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
|
||||
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
|
||||
Rect? rect = ffiModel.rect;
|
||||
|
||||
if (isMove) {
|
||||
|
|
@ -1705,7 +1938,7 @@ class InputModel {
|
|||
_windowRect != null &&
|
||||
!_isInCurrentWindow(x, y)) {
|
||||
final coords =
|
||||
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
|
||||
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
|
||||
if (coords != null) {
|
||||
isMove = false;
|
||||
canvas = coords.canvas;
|
||||
|
|
@ -1775,16 +2008,16 @@ class InputModel {
|
|||
}
|
||||
|
||||
Point? _handlePointerDevicePos(
|
||||
String kind,
|
||||
double x,
|
||||
double y,
|
||||
bool moveInCanvas,
|
||||
CanvasCoords canvas,
|
||||
Rect? rect,
|
||||
String evtType, {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
}) {
|
||||
String kind,
|
||||
double x,
|
||||
double y,
|
||||
bool moveInCanvas,
|
||||
CanvasCoords canvas,
|
||||
Rect? rect,
|
||||
String evtType, {
|
||||
bool onExit = false,
|
||||
int buttons = kPrimaryMouseButton,
|
||||
}) {
|
||||
if (rect == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1593,7 +1593,15 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
|
|||
get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en)
|
||||
}
|
||||
|
||||
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
||||
#[inline]
|
||||
fn has_shift_modifier(key_event: &KeyEvent) -> bool {
|
||||
key_event.modifiers.iter().any(|ck| {
|
||||
let v = ck.value();
|
||||
v == ControlKey::Shift.value() || v == ControlKey::RShift.value()
|
||||
})
|
||||
}
|
||||
|
||||
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool, uppercase_hint: bool) {
|
||||
// On Wayland with uinput mode, use clipboard for character input
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
|
|
@ -1608,7 +1616,35 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
||||
#[cfg(target_os = "windows")]
|
||||
if !_hotkey {
|
||||
let key = char_value_to_key(chr);
|
||||
|
||||
if down {
|
||||
if en.key_down(key).is_ok() {
|
||||
// The current Windows OS keyboard layout provided a valid key mapping for this
|
||||
// character, so it was sent through the normal key down/up path.
|
||||
// Only unmappable characters need the key_sequence() fallback below.
|
||||
// If something needs to be done right after the key_down it can be done here
|
||||
} else {
|
||||
// The character could not be mapped to a layout-dependent physical key in
|
||||
// the current OS keyboard layout. In that case, inject the resulting text
|
||||
// directly through key_sequence() as a fallback.
|
||||
if let Ok(chr) = char::try_from(chr) {
|
||||
let mut s = chr.to_string();
|
||||
if uppercase_hint {
|
||||
s = s.to_uppercase();
|
||||
}
|
||||
en.key_sequence(&s);
|
||||
};
|
||||
}
|
||||
} else {
|
||||
en.key_up(key);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if !_hotkey {
|
||||
if down {
|
||||
if let Ok(chr) = char::try_from(chr) {
|
||||
|
|
@ -1887,7 +1923,8 @@ fn legacy_keyboard_mode(evt: &KeyEvent) {
|
|||
|
||||
let record_key = chr as u64 + KEY_CHAR_START;
|
||||
record_pressed_key(KeysDown::EnigoKey(record_key), down);
|
||||
process_chr(&mut en, chr, down, has_hotkey_modifiers(evt))
|
||||
let uppercase_hint = has_shift_modifier(evt) || get_modifier_state(Key::CapsLock, &mut en);
|
||||
process_chr(&mut en, chr, down, has_hotkey_modifiers(evt), uppercase_hint)
|
||||
}
|
||||
Some(key_event::Union::Unicode(chr)) => {
|
||||
// Same as Chr: release Shift for Unicode input
|
||||
|
|
@ -2140,7 +2177,7 @@ pub fn handle_key_(evt: &KeyEvent) {
|
|||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
let mut _lock_mode_handler = None;
|
||||
let mut _lock_mode_handler = None;
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
match &evt.union {
|
||||
Some(key_event::Union::Unicode(..)) | Some(key_event::Union::Seq(..)) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue