forked from mirrors/rustdesk
fix(keyboard): wayland clipboard input prompt (#14700)
* fix(keyboard): wayland clipboard input prompt Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): clipboard input, remove unused code Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): Simple refactor Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): dialog, better enableAndContinue Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog consent Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): prompt text Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): text input 1. Use `keysym` for the installed version if possible. 2. Use the clipboard if the string cannot be fully handled by `keysym`. Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): dialog, title type Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): better decode_utf8_prefix() Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): better process_chr() Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): unit tests Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog, no icon Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog, Toast show the result Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input dialog, showToast() on persist failed Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt, better dialog Signed-off-by: fufesou <linlong1266@gmail.com> * fix(wayland): input prompt dialog, translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): better wayland clipboard input prompt Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): wayland clipboard, link external app Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): trivial changes Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): wayland clipboard input, dialog content Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): tranlsations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> * fix(input): translations Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
parent
00032854eb
commit
3217125dd3
60 changed files with 1142 additions and 101 deletions
|
|
@ -13,8 +13,64 @@ import 'package:flutter_hbb/models/model.dart';
|
|||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
bool isEditOsPassword = false;
|
||||
const String kPeerOptionAllowWaylandKeyboard = 'allow-wayland-keyboard';
|
||||
const String kWaylandKeyboardIssueUrl =
|
||||
'https://github.com/rustdesk/rustdesk/issues/14586';
|
||||
final Set<String> _waylandKeyboardPromptSuppressedConnectionIds = <String>{};
|
||||
|
||||
Future<bool> openWaylandKeyboardIssueUrl() {
|
||||
return launchUrl(
|
||||
Uri.parse(kWaylandKeyboardIssueUrl),
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
bool isWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
|
||||
return _waylandKeyboardPromptSuppressedConnectionIds.contains(connectionId);
|
||||
}
|
||||
|
||||
void setWaylandKeyboardPromptSuppressedForConnection(
|
||||
String connectionId, bool suppressed) {
|
||||
if (suppressed) {
|
||||
_waylandKeyboardPromptSuppressedConnectionIds.add(connectionId);
|
||||
} else {
|
||||
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
void clearWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
|
||||
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
|
||||
}
|
||||
|
||||
bool shouldShowWaylandKeyboardPrompt({
|
||||
required String connectionId,
|
||||
required bool isWaylandPeer,
|
||||
required bool allowWaylandKeyboardRemembered,
|
||||
}) {
|
||||
return isWaylandPeer &&
|
||||
!allowWaylandKeyboardRemembered &&
|
||||
!isWaylandKeyboardPromptSuppressedForConnection(connectionId);
|
||||
}
|
||||
|
||||
Widget waylandKeyboardScopeChip(BuildContext context, String text) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(color: colorScheme.primary.withOpacity(0.35)),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// macOS privacy mode blacks out all online displays, so switching the remote
|
||||
// display does not weaken the local privacy protection.
|
||||
|
|
@ -93,12 +149,179 @@ handleOsPasswordAction(
|
|||
}
|
||||
}
|
||||
|
||||
void showWaylandKeyboardInputWarningDialog(
|
||||
{required String id,
|
||||
required String connectionId,
|
||||
required FFI ffi,
|
||||
required Future<void> Function() onEnable}) {
|
||||
bool remember = false;
|
||||
bool consentInProgress = false;
|
||||
bool dialogClosed = false;
|
||||
|
||||
final dialogFuture = ffi.dialogManager.show((setState, close, context) {
|
||||
void safeSetState(VoidCallback fn) {
|
||||
if (dialogClosed) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setState(fn);
|
||||
} catch (e) {
|
||||
debugPrint('Ignore setState after dialog disposal: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void closeDialog() {
|
||||
if (dialogClosed) {
|
||||
return;
|
||||
}
|
||||
dialogClosed = true;
|
||||
close();
|
||||
}
|
||||
|
||||
Future<void> enableAndContinue() async {
|
||||
if (consentInProgress || dialogClosed) {
|
||||
return;
|
||||
}
|
||||
consentInProgress = true;
|
||||
safeSetState(() {});
|
||||
try {
|
||||
await onEnable();
|
||||
} catch (e, st) {
|
||||
debugPrint('Failed to enable Wayland keyboard input consent: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
consentInProgress = false;
|
||||
safeSetState(() {});
|
||||
return;
|
||||
}
|
||||
|
||||
ffi.inputModel.keyboardInputAllowed = true;
|
||||
var rememberPersisted = true;
|
||||
if (remember) {
|
||||
try {
|
||||
await bind.mainSetPeerOption(
|
||||
id: id,
|
||||
key: kPeerOptionAllowWaylandKeyboard,
|
||||
value: bool2option(kPeerOptionAllowWaylandKeyboard, true));
|
||||
} catch (e) {
|
||||
rememberPersisted = false;
|
||||
debugPrint('Failed to persist Wayland keyboard input consent: $e');
|
||||
}
|
||||
}
|
||||
// Always suppress prompt for current connection after explicit consent.
|
||||
setWaylandKeyboardPromptSuppressedForConnection(connectionId, true);
|
||||
closeDialog();
|
||||
if (remember && !rememberPersisted) {
|
||||
// It's a rare edge case that persisting the user's choice fails.
|
||||
// Failed to persist the user's choice, but still allow keyboard input for current session.
|
||||
showToast(translate('Failed'));
|
||||
}
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (consentInProgress) {
|
||||
return;
|
||||
}
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
return CustomAlertDialog(
|
||||
title: null,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
msgboxContent(
|
||||
'',
|
||||
'wayland-keyboard-input-disabled-tip',
|
||||
'wayland-keyboard-input-consent-tip',
|
||||
),
|
||||
SizedBox(height: isMobile ? 2 : 6),
|
||||
if (isMobile) ...[
|
||||
Text(
|
||||
translate('wayland-keyboard-input-applies-to-tip'),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
).marginOnly(bottom: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: [
|
||||
waylandKeyboardScopeChip(
|
||||
context, translate('Send clipboard keystrokes')),
|
||||
waylandKeyboardScopeChip(
|
||||
context, translate('wayland-soft-keyboard-input-label')),
|
||||
],
|
||||
).marginOnly(bottom: 10),
|
||||
],
|
||||
TextButton(
|
||||
onPressed: consentInProgress
|
||||
? null
|
||||
: () async {
|
||||
try {
|
||||
final opened = await openWaylandKeyboardIssueUrl();
|
||||
if (!opened) {
|
||||
// Opening this optional help link almost never fails in
|
||||
// normal desktop environments. Keep the result handled
|
||||
// for review hygiene, but avoid a low-value user toast.
|
||||
debugPrint('Failed to open Wayland keyboard issue URL');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Failed to open Wayland keyboard issue URL: $e');
|
||||
}
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.blue,
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Text(
|
||||
translate('Why this happens'),
|
||||
style: const TextStyle(decoration: TextDecoration.underline),
|
||||
),
|
||||
).marginOnly(bottom: 6),
|
||||
CheckboxListTile(
|
||||
value: remember,
|
||||
dense: true,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
title: Text(translate('remember-wayland-keyboard-choice-tip')),
|
||||
onChanged: consentInProgress
|
||||
? null
|
||||
: (v) {
|
||||
safeSetState(() => remember = v == true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
dialogButton(
|
||||
'Cancel',
|
||||
onPressed: consentInProgress ? null : cancel,
|
||||
isOutline: true,
|
||||
),
|
||||
dialogButton(
|
||||
'OK',
|
||||
onPressed:
|
||||
consentInProgress ? null : () => unawaited(enableAndContinue()),
|
||||
),
|
||||
],
|
||||
onCancel: consentInProgress ? null : cancel,
|
||||
onSubmit: consentInProgress ? null : () => unawaited(enableAndContinue()),
|
||||
);
|
||||
}, clickMaskDismiss: false, backDismiss: false);
|
||||
unawaited(dialogFuture.whenComplete(() => dialogClosed = true));
|
||||
}
|
||||
|
||||
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
||||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final perms = ffiModel.permissions;
|
||||
final sessionId = ffi.sessionId;
|
||||
final isDefaultConn = ffi.connType == ConnType.defaultConn;
|
||||
final isWaylandPeer = pi.platform == kPeerPlatformLinux && pi.isWayland;
|
||||
|
||||
List<TTextMenu> v = [];
|
||||
// elevation
|
||||
|
|
@ -148,11 +371,60 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||
v.add(TTextMenu(
|
||||
child: Text(translate('Send clipboard keystrokes')),
|
||||
onPressed: () async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
bind.sessionInputString(
|
||||
sessionId: sessionId, value: data.text ?? "");
|
||||
Future<void> sendClipboardKeystrokes() async {
|
||||
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null) {
|
||||
bind.sessionInputString(
|
||||
sessionId: sessionId, value: data.text ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
final allowWaylandKeyboard =
|
||||
mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard);
|
||||
if (shouldShowWaylandKeyboardPrompt(
|
||||
connectionId: sessionId.toString(),
|
||||
isWaylandPeer: isWaylandPeer,
|
||||
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
|
||||
)) {
|
||||
ffi.inputModel.keyboardInputAllowed = false;
|
||||
showWaylandKeyboardInputWarningDialog(
|
||||
id: id,
|
||||
connectionId: sessionId.toString(),
|
||||
ffi: ffi,
|
||||
onEnable: sendClipboardKeystrokes,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await sendClipboardKeystrokes();
|
||||
}));
|
||||
}
|
||||
if (isDefaultConn &&
|
||||
isWaylandPeer &&
|
||||
(mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard) ||
|
||||
isWaylandKeyboardPromptSuppressedForConnection(
|
||||
sessionId.toString()))) {
|
||||
v.add(TTextMenu(
|
||||
child: Text(translate('wayland-keyboard-input-reset-choice-tip')),
|
||||
onPressed: () async {
|
||||
var persistedCleared = false;
|
||||
try {
|
||||
await bind.mainSetPeerOption(
|
||||
id: id,
|
||||
key: kPeerOptionAllowWaylandKeyboard,
|
||||
value: bool2option(kPeerOptionAllowWaylandKeyboard, false));
|
||||
persistedCleared = true;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'Failed to clear persisted Wayland keyboard permission: $e');
|
||||
} finally {
|
||||
clearWaylandKeyboardPromptSuppressedForConnection(
|
||||
sessionId.toString());
|
||||
ffi.inputModel.keyboardInputAllowed = false;
|
||||
if (isMobile) {
|
||||
await ffi.invokeMethod("enable_soft_keyboard", false);
|
||||
}
|
||||
}
|
||||
showToast(translate(persistedCleared ? 'Successful' : 'Failed'));
|
||||
}));
|
||||
}
|
||||
// reset canvas
|
||||
|
|
@ -766,7 +1038,8 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||
final ffiModel = ffi.ffiModel;
|
||||
final pi = ffiModel.pi;
|
||||
final sessionId = ffi.sessionId;
|
||||
final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false;
|
||||
final hasPrivacyModePermission =
|
||||
ffiModel.permissions['privacy_mode'] != false;
|
||||
|
||||
// Backend revocation already attempts to turn privacy mode off.
|
||||
// Still keep this menu when privacy mode is active, so users can turn it off
|
||||
|
|
@ -776,8 +1049,8 @@ List<TToggleMenu> toolbarPrivacyMode(
|
|||
}
|
||||
|
||||
getDefaultMenu(Future<void> Function(SessionID sid, String opt) toggleFunc) {
|
||||
final enabled =
|
||||
!ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||
final enabled = !ffiModel.viewOnly &&
|
||||
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
|
||||
return TToggleMenu(
|
||||
value: privacyModeState.isNotEmpty,
|
||||
onChanged: enabled
|
||||
|
|
|
|||
|
|
@ -101,6 +101,9 @@ class _RemotePageState extends State<RemotePage>
|
|||
Function(bool)? _onEnterOrLeaveImage4Toolbar;
|
||||
|
||||
late FFI _ffi;
|
||||
Worker? _waylandKeyboardModeWorker;
|
||||
bool _waylandKeyboardModeNormalized = false;
|
||||
bool _waylandKeyboardModeNormalizing = false;
|
||||
|
||||
SessionID get sessionId => _ffi.sessionId;
|
||||
|
||||
|
|
@ -178,6 +181,48 @@ class _RemotePageState extends State<RemotePage>
|
|||
// Register callback to cancel debounce timer when relative mouse mode is disabled
|
||||
_ffi.inputModel.onRelativeMouseModeDisabled =
|
||||
_cancelPointerLockCenterDebounceTimer;
|
||||
|
||||
_waylandKeyboardModeWorker = ever(_ffi.ffiModel.pi.isSet, (bool isSet) {
|
||||
if (isSet) {
|
||||
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
|
||||
}
|
||||
});
|
||||
if (_ffi.ffiModel.pi.isSet.value) {
|
||||
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _normalizeWaylandKeyboardModeIfNeeded() async {
|
||||
if (!mounted ||
|
||||
_waylandKeyboardModeNormalized ||
|
||||
_waylandKeyboardModeNormalizing) {
|
||||
return;
|
||||
}
|
||||
_waylandKeyboardModeNormalizing = true;
|
||||
try {
|
||||
final pi = _ffi.ffiModel.pi;
|
||||
if (pi.platform != kPeerPlatformLinux || !pi.isWayland) return;
|
||||
final mapSupported = bind.sessionIsKeyboardModeSupported(
|
||||
sessionId: sessionId, mode: kKeyMapMode);
|
||||
if (!mapSupported) return;
|
||||
final current = await bind.sessionGetKeyboardMode(sessionId: sessionId);
|
||||
if (!mounted) return;
|
||||
if (current == kKeyMapMode) {
|
||||
_waylandKeyboardModeNormalized = true;
|
||||
return;
|
||||
}
|
||||
await bind.sessionSetKeyboardMode(
|
||||
sessionId: sessionId, value: kKeyMapMode);
|
||||
if (!mounted) return;
|
||||
await _ffi.inputModel.updateKeyboardMode();
|
||||
if (!mounted) return;
|
||||
_waylandKeyboardModeNormalized = true;
|
||||
} catch (e, st) {
|
||||
debugPrint('Failed to normalize Wayland keyboard mode: $e');
|
||||
debugPrintStack(stackTrace: st);
|
||||
} finally {
|
||||
_waylandKeyboardModeNormalizing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the pointer lock center debounce timer
|
||||
|
|
@ -318,6 +363,7 @@ class _RemotePageState extends State<RemotePage>
|
|||
|
||||
_pointerLockCenterDebounceTimer?.cancel();
|
||||
_pointerLockCenterDebounceTimer = null;
|
||||
_waylandKeyboardModeWorker?.dispose();
|
||||
// Clear callback reference to prevent memory leaks and stale references
|
||||
_ffi.inputModel.onRelativeMouseModeDisabled = null;
|
||||
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
|
||||
|
|
@ -331,6 +377,9 @@ class _RemotePageState extends State<RemotePage>
|
|||
_ffi.imageModel.disposeImage();
|
||||
_ffi.cursorModel.disposeImages();
|
||||
_rawKeyFocusNode.dispose();
|
||||
if (closeSession) {
|
||||
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
|
||||
}
|
||||
await _ffi.close(closeSession: closeSession);
|
||||
_timer?.cancel();
|
||||
_ffi.dialogManager.dismissAll();
|
||||
|
|
|
|||
|
|
@ -2324,18 +2324,8 @@ class _KeyboardMenu extends StatelessWidget {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (pi.isWayland) {
|
||||
// Legacy mode is hidden on desktop control side because dead keys
|
||||
// don't work properly on Wayland. When the control side is mobile,
|
||||
// Legacy mode is used automatically (mobile always sends Legacy events).
|
||||
if (mode.key == kKeyLegacyMode) {
|
||||
continue;
|
||||
}
|
||||
// Translate mode requires server >= 1.4.6.
|
||||
if (mode.key == kKeyTranslateMode &&
|
||||
versionCmp(pi.version, '1.4.6') < 0) {
|
||||
continue;
|
||||
}
|
||||
if (pi.isWayland && mode.key != kKeyMapMode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = translate(mode.menu);
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
final FocusNode _physicalFocusNode = FocusNode();
|
||||
var _showEdit = false; // use soft keyboard
|
||||
|
||||
Worker? _waylandKeyboardGateWorker;
|
||||
bool _waylandKeyboardGateInitialized = false;
|
||||
|
||||
InputModel get inputModel => gFFI.inputModel;
|
||||
SessionID get sessionId => gFFI.sessionId;
|
||||
|
||||
|
|
@ -121,6 +124,20 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
isKeyboardVisible: keyboardVisibilityController.isVisible);
|
||||
});
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
inputModel.keyboardInputAllowed = true;
|
||||
|
||||
// Wayland sessions may use clipboard-based text input on the controlled side.
|
||||
// Require explicit user confirmation before allowing soft-keyboard and
|
||||
// clipboard-assisted text input. Physical keyboard events are not gated here.
|
||||
_waylandKeyboardGateWorker = ever(gFFI.ffiModel.pi.isSet, (bool isSet) {
|
||||
if (isSet) {
|
||||
_initWaylandKeyboardGateIfNeeded();
|
||||
}
|
||||
});
|
||||
if (gFFI.ffiModel.pi.isSet.value) {
|
||||
_initWaylandKeyboardGateIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -143,6 +160,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
_mobileFocusNode.dispose();
|
||||
_physicalFocusNode.dispose();
|
||||
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
|
||||
_waylandKeyboardGateWorker?.dispose();
|
||||
inputModel.keyboardInputAllowed = true;
|
||||
await gFFI.close();
|
||||
_timer?.cancel();
|
||||
_iosKeyboardWorkaroundTimer?.cancel();
|
||||
|
|
@ -171,6 +191,40 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
gFFI.invokeMethod("try_sync_clipboard");
|
||||
}
|
||||
|
||||
bool _shouldGateKeyboardForWayland() {
|
||||
if (!(isAndroid || isIOS)) return false;
|
||||
final pi = gFFI.ffiModel.pi;
|
||||
return pi.platform == kPeerPlatformLinux && pi.isWayland;
|
||||
}
|
||||
|
||||
void _initWaylandKeyboardGateIfNeeded() {
|
||||
if (!mounted) return;
|
||||
if (_waylandKeyboardGateInitialized) return;
|
||||
if (!_shouldGateKeyboardForWayland()) return;
|
||||
|
||||
_waylandKeyboardGateInitialized = true;
|
||||
|
||||
final allowWaylandKeyboard =
|
||||
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
|
||||
if (!shouldShowWaylandKeyboardPrompt(
|
||||
connectionId: sessionId.toString(),
|
||||
isWaylandPeer: _shouldGateKeyboardForWayland(),
|
||||
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
|
||||
)) {
|
||||
inputModel.keyboardInputAllowed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
inputModel.keyboardInputAllowed = false;
|
||||
|
||||
// Ensure soft keyboard is not active before user confirms.
|
||||
_showEdit = false;
|
||||
gFFI.invokeMethod("enable_soft_keyboard", false);
|
||||
_mobileFocusNode.unfocus();
|
||||
_physicalFocusNode.requestFocus();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
@ -302,7 +356,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
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);
|
||||
openKeyboard();
|
||||
_openKeyboardUnlocked();
|
||||
return;
|
||||
}
|
||||
bind.sessionInputString(sessionId: sessionId, value: content);
|
||||
|
|
@ -314,6 +368,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
|
||||
// handle mobile virtual keyboard
|
||||
void handleSoftKeyboardInput(String newValue) {
|
||||
if (!inputModel.keyboardInputAllowed) {
|
||||
return;
|
||||
}
|
||||
if (isIOS) {
|
||||
_handleIOSSoftKeyboardInput(newValue);
|
||||
} else {
|
||||
|
|
@ -322,6 +379,9 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
void inputChar(String char) {
|
||||
if (!inputModel.keyboardInputAllowed) {
|
||||
return;
|
||||
}
|
||||
if (char == '\n') {
|
||||
char = 'VK_RETURN';
|
||||
} else if (char == ' ') {
|
||||
|
|
@ -331,6 +391,29 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
|
|||
}
|
||||
|
||||
void openKeyboard() {
|
||||
final allowWaylandKeyboard =
|
||||
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
|
||||
if (shouldShowWaylandKeyboardPrompt(
|
||||
connectionId: sessionId.toString(),
|
||||
isWaylandPeer: _shouldGateKeyboardForWayland(),
|
||||
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
|
||||
)) {
|
||||
inputModel.keyboardInputAllowed = false;
|
||||
showWaylandKeyboardInputWarningDialog(
|
||||
id: widget.id,
|
||||
connectionId: sessionId.toString(),
|
||||
ffi: gFFI,
|
||||
onEnable: () async {
|
||||
_openKeyboardUnlocked();
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
_openKeyboardUnlocked();
|
||||
}
|
||||
|
||||
void _openKeyboardUnlocked() {
|
||||
inputModel.keyboardInputAllowed = true;
|
||||
gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||
// destroy first, so that our _value trick can work
|
||||
_value = initText;
|
||||
|
|
|
|||
|
|
@ -474,6 +474,10 @@ class InputModel {
|
|||
|
||||
late final SessionID sessionId;
|
||||
|
||||
// Local gate for clipboard-assisted input flows on mobile Wayland dialogs.
|
||||
// It should not block physical keyboard events.
|
||||
bool keyboardInputAllowed = true;
|
||||
|
||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||
String get id => parent.target?.id ?? '';
|
||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#[cfg(not(target_os = "android"))]
|
||||
use arboard::{ClipboardData, ClipboardFormat};
|
||||
#[cfg(target_os = "linux")]
|
||||
use arboard::{LinuxClipboardKind, SetExtLinux};
|
||||
use hbb_common::{bail, log, message_proto::*, ResultType};
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
|
|
@ -54,6 +56,27 @@ pub fn check_clipboard(
|
|||
side: ClipboardSide,
|
||||
force: bool,
|
||||
) -> Option<Message> {
|
||||
let (msg, clipboards) = read_clipboard_message(ctx, side, force)?;
|
||||
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn peek_clipboard(
|
||||
ctx: &mut Option<ClipboardContext>,
|
||||
side: ClipboardSide,
|
||||
force: bool,
|
||||
) -> Option<Message> {
|
||||
let (msg, _) = read_clipboard_message(ctx, side, force)?;
|
||||
Some(msg)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn read_clipboard_message(
|
||||
ctx: &mut Option<ClipboardContext>,
|
||||
side: ClipboardSide,
|
||||
force: bool,
|
||||
) -> Option<(Message, MultiClipboards)> {
|
||||
if ctx.is_none() {
|
||||
*ctx = ClipboardContext::new().ok();
|
||||
}
|
||||
|
|
@ -64,8 +87,7 @@ pub fn check_clipboard(
|
|||
let mut msg = Message::new();
|
||||
let clipboards = proto::create_multi_clipboards(content);
|
||||
msg.set_multi_clipboards(clipboards.clone());
|
||||
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
|
||||
return Some(msg);
|
||||
return Some((msg, clipboards));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
@ -219,10 +241,7 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
|
|||
}
|
||||
}
|
||||
if let Some(ctx) = ctx.as_mut() {
|
||||
to_update_data.push(ClipboardData::Special((
|
||||
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
|
||||
side.get_owner_data(),
|
||||
)));
|
||||
to_update_data = append_owner_marker(to_update_data, side);
|
||||
if let Err(e) = ctx.set(&to_update_data) {
|
||||
log::debug!("Failed to set clipboard: {}", e);
|
||||
} else {
|
||||
|
|
@ -231,6 +250,29 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn append_owner_marker(mut data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<ClipboardData> {
|
||||
data.push(ClipboardData::Special((
|
||||
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
|
||||
side.get_owner_data(),
|
||||
)));
|
||||
data
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_text_clipboard_with_owner_sync(text: &str, side: ClipboardSide) -> ResultType<()> {
|
||||
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
|
||||
if ctx.is_none() {
|
||||
*ctx = Some(ClipboardContext::new()?);
|
||||
}
|
||||
let clipboard_ctx = match ctx.as_mut() {
|
||||
Some(ctx) => ctx,
|
||||
None => bail!("Failed to create clipboard context"),
|
||||
};
|
||||
let data = append_owner_marker(vec![ClipboardData::Text(text.to_owned())], side);
|
||||
clipboard_ctx.set_with_owner_marker_for_linux(&data)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
|
||||
std::thread::spawn(move || {
|
||||
|
|
@ -382,6 +424,24 @@ impl ClipboardContext {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_with_owner_marker_for_linux(&mut self, data: &[ClipboardData]) -> ResultType<()> {
|
||||
let _lock = ARBOARD_MTX.lock().unwrap();
|
||||
self.inner
|
||||
.set()
|
||||
.clipboard(LinuxClipboardKind::Clipboard)
|
||||
.formats(data)?;
|
||||
if let Err(e) = self
|
||||
.inner
|
||||
.set()
|
||||
.clipboard(LinuxClipboardKind::Primary)
|
||||
.formats(data)
|
||||
{
|
||||
log::warn!("Failed to set PRIMARY clipboard with owner marker: {}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
|
||||
fn get_file_urls_set_by_rustdesk(
|
||||
data: Vec<ClipboardData>,
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", "服务器要求显式部署此设备。是否立即部署?"),
|
||||
("The server does not require explicit deployment.", "服务器不需要显式部署。"),
|
||||
("Unknown response.", "未知响应。"),
|
||||
("wayland-keyboard-input-disabled-tip", "允许键盘输入?"),
|
||||
("wayland-keyboard-input-consent-tip", "你在这台远程电脑上输入的内容(包括密码)可能被远程电脑上的其他程序读取。"),
|
||||
("wayland-keyboard-input-applies-to-tip", "此选择适用于:"),
|
||||
("wayland-soft-keyboard-input-label", "软键盘输入"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "重置键盘输入选择"),
|
||||
("remember-wayland-keyboard-choice-tip", "以后对这台远程电脑不再询问"),
|
||||
("Why this happens", "了解原因"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,5 +276,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("preset-password-in-use-tip", "Preset password is currently in use."),
|
||||
("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"),
|
||||
("server_requires_deployment_tip", "The server requires this device to be deployed explicitly. Deploy now?"),
|
||||
("wayland-keyboard-input-disabled-tip", "Allow keyboard input?"),
|
||||
("wayland-keyboard-input-consent-tip", "What you type on this remote computer (including passwords) could be read by other apps on it."),
|
||||
("wayland-keyboard-input-applies-to-tip", "This choice applies to:"),
|
||||
("wayland-soft-keyboard-input-label", "Soft keyboard input"),
|
||||
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
|
||||
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", "Il server richiede che questo dispositivo venga distribuito in modo esplicito.\nVuoi distribuirlo?"),
|
||||
("The server does not require explicit deployment.", "Il server non richiede una distribuzione esplicita."),
|
||||
("Unknown response.", "Risposta sconosciuta"),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -751,5 +751,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
|||
("server_requires_deployment_tip", ""),
|
||||
("The server does not require explicit deployment.", ""),
|
||||
("Unknown response.", ""),
|
||||
("wayland-keyboard-input-disabled-tip", ""),
|
||||
("wayland-keyboard-input-consent-tip", ""),
|
||||
("wayland-keyboard-input-applies-to-tip", ""),
|
||||
("wayland-soft-keyboard-input-label", ""),
|
||||
("wayland-keyboard-input-reset-choice-tip", ""),
|
||||
("remember-wayland-keyboard-choice-tip", ""),
|
||||
("Why this happens", ""),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use super::*;
|
|||
#[cfg(not(target_os = "android"))]
|
||||
use crate::clipboard::clipboard_listener;
|
||||
#[cfg(not(target_os = "android"))]
|
||||
pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide};
|
||||
pub use crate::clipboard::{ClipboardContext, ClipboardSide};
|
||||
pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME};
|
||||
#[cfg(windows)]
|
||||
use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data};
|
||||
|
|
@ -109,6 +109,62 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES: usize =
|
||||
super::input_service::WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS * 4;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn decode_utf8_prefix(bytes: &[u8]) -> Option<String> {
|
||||
let end = bytes.len().min(WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
|
||||
let slice = &bytes[..end];
|
||||
match std::str::from_utf8(slice) {
|
||||
Ok(text) => Some(text.to_owned()),
|
||||
Err(e) => {
|
||||
if e.error_len().is_some() {
|
||||
return None;
|
||||
}
|
||||
let valid_up_to = e.valid_up_to();
|
||||
std::str::from_utf8(&slice[..valid_up_to])
|
||||
.ok()
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn decode_text_clipboard(clipboard: &Clipboard) -> Option<String> {
|
||||
if clipboard.format.enum_value() != Ok(ClipboardFormat::Text) {
|
||||
return None;
|
||||
}
|
||||
if clipboard.compress {
|
||||
let bytes = hbb_common::compress::decompress(&clipboard.content);
|
||||
return decode_utf8_prefix(&bytes);
|
||||
}
|
||||
decode_utf8_prefix(&clipboard.content)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn should_skip_wayland_clipboard_sync(msg: &Message) -> bool {
|
||||
if crate::platform::linux::is_x11() {
|
||||
return false;
|
||||
}
|
||||
let is_recent_wayland_input = |clipboard: &Clipboard| -> bool {
|
||||
let Some(text) = decode_text_clipboard(clipboard) else {
|
||||
return false;
|
||||
};
|
||||
super::input_service::is_recent_wayland_clipboard_input(&text)
|
||||
};
|
||||
|
||||
match &msg.union {
|
||||
Some(message::Union::Clipboard(clipboard)) => is_recent_wayland_input(clipboard),
|
||||
Some(message::Union::MultiClipboards(multi_clipboards)) => multi_clipboards
|
||||
.clipboards
|
||||
.iter()
|
||||
.any(is_recent_wayland_input),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
impl Handler {
|
||||
#[cfg(feature = "unix-file-copy-paste")]
|
||||
|
|
@ -172,7 +228,19 @@ impl Handler {
|
|||
}
|
||||
}
|
||||
|
||||
check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let msg = crate::clipboard::peek_clipboard(&mut self.ctx, ClipboardSide::Host, false)?;
|
||||
if should_skip_wayland_clipboard_sync(&msg) {
|
||||
log::debug!("Skip clipboard sync for recent Wayland keyboard injection");
|
||||
return None;
|
||||
}
|
||||
return Some(msg);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
crate::clipboard::check_clipboard(&mut self.ctx, ClipboardSide::Host, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Read clipboard data from cm using ipc.
|
||||
|
|
@ -272,3 +340,46 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> {
|
|||
CLIPBOARD_SERVICE_OK.store(false, Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(target_os = "linux")]
|
||||
mod tests {
|
||||
use super::{decode_utf8_prefix, WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES};
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_returns_text_for_valid_utf8() {
|
||||
let text = "hello-مرحبا";
|
||||
assert_eq!(decode_utf8_prefix(text.as_bytes()), Some(text.to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_returns_none_for_invalid_utf8_sequence() {
|
||||
let bytes = b"ab\xffcd";
|
||||
assert_eq!(decode_utf8_prefix(bytes), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_trims_incomplete_utf8_suffix() {
|
||||
let bytes = vec![b'a', 0xE4, 0xB8];
|
||||
assert_eq!(decode_utf8_prefix(&bytes), Some("a".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_applies_max_bytes_limit() {
|
||||
let bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES + 8];
|
||||
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
|
||||
assert_eq!(result.len(), WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8_prefix_keeps_utf8_boundary_when_limited() {
|
||||
let mut bytes = vec![b'a'; WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1];
|
||||
bytes.extend_from_slice("ا".as_bytes());
|
||||
let result = decode_utf8_prefix(&bytes).expect("expected decoded prefix");
|
||||
assert_eq!(
|
||||
result.len(),
|
||||
WAYLAND_CLIPBOARD_SKIP_CHECK_MAX_UTF8_BYTES - 1
|
||||
);
|
||||
assert!(result.chars().all(|c| c == 'a'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,6 +457,12 @@ lazy_static::lazy_static! {
|
|||
static ref RELATIVE_MOUSE_CONNS: Arc<Mutex<std::collections::HashSet<i32>>> = Default::default();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
lazy_static::lazy_static! {
|
||||
static ref WAYLAND_CLIPBOARD_INPUT_RECORDS: Arc<Mutex<Vec<(Instant, String)>>> =
|
||||
Default::default();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_relative_mouse_active(conn: i32, active: bool) {
|
||||
let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap();
|
||||
|
|
@ -1594,15 +1600,28 @@ fn need_to_uppercase(en: &mut Enigo) -> bool {
|
|||
}
|
||||
|
||||
fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
||||
// On Wayland with uinput mode, use clipboard for character input
|
||||
// On Wayland with uinput mode:
|
||||
// - ASCII printable: input via key events (custom keyboard path, e.g. portal keysym)
|
||||
// - Non-ASCII: input via clipboard paste
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
// Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed)
|
||||
if !is_hotkey_modifier_pressed(en) {
|
||||
if down {
|
||||
if let Ok(c) = char::try_from(chr) {
|
||||
if let Ok(c) = char::try_from(chr) {
|
||||
if is_ascii_printable(c) {
|
||||
if down {
|
||||
en.key_down(Key::Layout(c)).ok();
|
||||
} else {
|
||||
en.key_up(Key::Layout(c));
|
||||
}
|
||||
} else if down {
|
||||
input_char_via_clipboard_server(en, c);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Ignore invalid unicode scalar in Wayland+uinput path: {}",
|
||||
chr
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1637,11 +1656,17 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) {
|
|||
}
|
||||
|
||||
fn process_unicode(en: &mut Enigo, chr: u32) {
|
||||
// On Wayland with uinput mode, use clipboard for character input
|
||||
// On Wayland with uinput mode:
|
||||
// - ASCII printable: input via key sequence (custom keyboard path)
|
||||
// - Non-ASCII: input via clipboard paste
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
if let Ok(c) = char::try_from(chr) {
|
||||
input_char_via_clipboard_server(en, c);
|
||||
if is_ascii_printable(c) {
|
||||
en.key_sequence(&c.to_string());
|
||||
} else {
|
||||
input_char_via_clipboard_server(en, c);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -1652,10 +1677,16 @@ fn process_unicode(en: &mut Enigo, chr: u32) {
|
|||
}
|
||||
|
||||
fn process_seq(en: &mut Enigo, sequence: &str) {
|
||||
// On Wayland with uinput mode, use clipboard for text input
|
||||
// On Wayland with uinput mode:
|
||||
// - pure ASCII printable sequence: input via key sequence (custom keyboard path)
|
||||
// - any non-ASCII present: input whole sequence via clipboard to preserve order
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() && wayland_use_uinput() {
|
||||
input_text_via_clipboard_server(en, sequence);
|
||||
if sequence.chars().all(is_ascii_printable) {
|
||||
en.key_sequence(sequence);
|
||||
} else {
|
||||
input_text_via_clipboard_server(en, sequence);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1668,40 +1699,103 @@ fn process_seq(en: &mut Enigo, sequence: &str) {
|
|||
/// this delay may be insufficient, but there is no reliable alternative mechanism.
|
||||
#[cfg(target_os = "linux")]
|
||||
const CLIPBOARD_SYNC_DELAY_MS: u64 = 50;
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW: Duration = Duration::from_secs(1);
|
||||
#[cfg(target_os = "linux")]
|
||||
const WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS: usize = 256;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) const WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS: usize = 1024;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn cleanup_wayland_clipboard_input_records(records: &mut Vec<(Instant, String)>, now: Instant) {
|
||||
records.retain(|(created_at, _)| {
|
||||
now.saturating_duration_since(*created_at) <= WAYLAND_CLIPBOARD_INPUT_FILTER_WINDOW
|
||||
});
|
||||
let len = records.len();
|
||||
if len > WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS {
|
||||
records.drain(0..(len - WAYLAND_CLIPBOARD_INPUT_MAX_RECORDS));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn normalize_wayland_clipboard_input_text(text: &str) -> String {
|
||||
text.chars()
|
||||
.take(WAYLAND_CLIPBOARD_INPUT_MAX_TEXT_CHARS)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn get_wayland_clipboard_input_normalized_text(text: &str) -> Option<String> {
|
||||
let normalized = normalize_wayland_clipboard_input_text(text);
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(normalized)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn record_wayland_clipboard_input_for_sync_filter(text: &str) -> Option<(Instant, String)> {
|
||||
if text.is_empty() || crate::platform::linux::is_x11() {
|
||||
return None;
|
||||
}
|
||||
let normalized = get_wayland_clipboard_input_normalized_text(text)?;
|
||||
let now = Instant::now();
|
||||
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
||||
cleanup_wayland_clipboard_input_records(&mut records, now);
|
||||
records.push((now, normalized.clone()));
|
||||
Some((now, normalized))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
fn rollback_wayland_clipboard_input_record(record: (Instant, String)) {
|
||||
let (created_at, normalized) = record;
|
||||
let now = Instant::now();
|
||||
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
||||
cleanup_wayland_clipboard_input_records(&mut records, now);
|
||||
if let Some(pos) = records
|
||||
.iter()
|
||||
.rposition(|(record_created_at, record_normalized)| {
|
||||
*record_created_at == created_at && *record_normalized == normalized
|
||||
})
|
||||
{
|
||||
records.remove(pos);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(super) fn is_recent_wayland_clipboard_input(text: &str) -> bool {
|
||||
if text.is_empty() || crate::platform::linux::is_x11() {
|
||||
return false;
|
||||
}
|
||||
let Some(normalized) = get_wayland_clipboard_input_normalized_text(text) else {
|
||||
return false;
|
||||
};
|
||||
let now = Instant::now();
|
||||
let mut records = WAYLAND_CLIPBOARD_INPUT_RECORDS.lock().unwrap();
|
||||
cleanup_wayland_clipboard_input_records(&mut records, now);
|
||||
records
|
||||
.iter()
|
||||
.any(|(_, record_normalized)| record_normalized == &normalized)
|
||||
}
|
||||
|
||||
/// Internal: Set clipboard content without delay.
|
||||
/// Returns true if clipboard was set successfully.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_clipboard_content(text: &str) -> bool {
|
||||
use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux};
|
||||
|
||||
let mut clipboard = match Clipboard::new() {
|
||||
Ok(cb) => cb,
|
||||
Err(e) => {
|
||||
log::error!("set_clipboard_content: failed to create clipboard: {:?}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Set both CLIPBOARD and PRIMARY selections
|
||||
// Terminal uses PRIMARY for Shift+Insert, GUI apps use CLIPBOARD
|
||||
if let Err(e) = clipboard
|
||||
.set()
|
||||
.clipboard(LinuxClipboardKind::Clipboard)
|
||||
.text(text.to_owned())
|
||||
{
|
||||
log::error!("set_clipboard_content: failed to set CLIPBOARD: {:?}", e);
|
||||
if let Err(e) = crate::clipboard::set_text_clipboard_with_owner_sync(
|
||||
text,
|
||||
crate::clipboard::ClipboardSide::Host,
|
||||
) {
|
||||
log::error!(
|
||||
"set_clipboard_content: failed to set clipboard with owner marker: {:?}",
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if let Err(e) = clipboard
|
||||
.set()
|
||||
.clipboard(LinuxClipboardKind::Primary)
|
||||
.text(text.to_owned())
|
||||
{
|
||||
log::warn!("set_clipboard_content: failed to set PRIMARY: {:?}", e);
|
||||
// Continue anyway, CLIPBOARD might work
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -1714,7 +1808,11 @@ fn set_clipboard_content(text: &str) -> bool {
|
|||
#[cfg(target_os = "linux")]
|
||||
#[inline]
|
||||
pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool {
|
||||
let record = record_wayland_clipboard_input_for_sync_filter(text);
|
||||
if !set_clipboard_content(text) {
|
||||
if let Some(record) = record {
|
||||
rollback_wayland_clipboard_input_record(record);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS));
|
||||
|
|
@ -1916,49 +2014,53 @@ fn translate_process_code(code: u32, down: bool) {
|
|||
fn translate_keyboard_mode(evt: &KeyEvent) {
|
||||
match &evt.union {
|
||||
Some(key_event::Union::Seq(seq)) => {
|
||||
// On Wayland, handle character input directly in this (--server) process using clipboard.
|
||||
// This function runs in the --server process (logged-in user session), which has
|
||||
// WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here.
|
||||
//
|
||||
// Why not let it go through uinput IPC:
|
||||
// 1. For uinput mode: the uinput service thread runs in the --service (root) process,
|
||||
// which typically lacks user session environment. Clipboard operations there are
|
||||
// unreliable. Handling clipboard here avoids that issue.
|
||||
// 2. For RDP input mode: Portal's notify_keyboard_keysym API interprets keysyms
|
||||
// based on its internal modifier state, which may not match our released state.
|
||||
// Using clipboard bypasses this issue entirely.
|
||||
// On Wayland:
|
||||
// - uinput mode (--service): keep clipboard handling in this process because
|
||||
// clipboard is unreliable in root service context.
|
||||
// - rdp_input mode (--server): forward sequence to custom keyboard handler so
|
||||
// ASCII can use Portal keysym and non-ASCII can use clipboard.
|
||||
#[cfg(target_os = "linux")]
|
||||
if !crate::platform::linux::is_x11() {
|
||||
let mut en = ENIGO.lock().unwrap();
|
||||
|
||||
// Check if this is a hotkey (Ctrl/Alt/Meta pressed)
|
||||
// For hotkeys, we send character-based key events via Enigo instead of
|
||||
// using the clipboard. This relies on the local keyboard layout for
|
||||
// mapping characters to physical keys.
|
||||
// This assumes client and server use the same keyboard layout (common case).
|
||||
// Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work
|
||||
// correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT.
|
||||
// This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin
|
||||
// characters which are mappable on most keyboard layouts.
|
||||
if is_hotkey_modifier_pressed(&mut en) {
|
||||
// For hotkeys, send character-based key events via Enigo.
|
||||
// This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT).
|
||||
for chr in seq.chars() {
|
||||
if !is_ascii_printable(chr) {
|
||||
log::warn!(
|
||||
"Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts"
|
||||
);
|
||||
}
|
||||
en.key_click(Key::Layout(chr));
|
||||
}
|
||||
if wayland_use_rdp_input() {
|
||||
release_shift_for_char_input(&mut en);
|
||||
en.key_sequence(seq);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal text input: release Shift and use clipboard
|
||||
release_shift_for_char_input(&mut en);
|
||||
if wayland_use_uinput() {
|
||||
// Check if this is a hotkey (Ctrl/Alt/Meta pressed)
|
||||
// For hotkeys, we send character-based key events via Enigo instead of
|
||||
// using the clipboard. This relies on the local keyboard layout for
|
||||
// mapping characters to physical keys.
|
||||
// This assumes client and server use the same keyboard layout (common case).
|
||||
// Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work
|
||||
// correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT.
|
||||
// This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin
|
||||
// characters which are mappable on most keyboard layouts.
|
||||
if is_hotkey_modifier_pressed(&mut en) {
|
||||
// For hotkeys, send character-based key events via Enigo.
|
||||
// This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT).
|
||||
for chr in seq.chars() {
|
||||
if !is_ascii_printable(chr) {
|
||||
log::warn!(
|
||||
"Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts"
|
||||
);
|
||||
}
|
||||
en.key_click(Key::Layout(chr));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
input_text_via_clipboard_server(&mut en, seq);
|
||||
return;
|
||||
// Normal text input: release Shift and use clipboard
|
||||
release_shift_for_char_input(&mut en);
|
||||
if seq.chars().all(is_ascii_printable) {
|
||||
en.key_sequence(seq);
|
||||
} else {
|
||||
input_text_via_clipboard_server(&mut en, seq);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fr -> US
|
||||
|
|
|
|||
|
|
@ -118,6 +118,23 @@ pub mod client {
|
|||
}
|
||||
|
||||
fn key_sequence(&mut self, s: &str) {
|
||||
if s.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep ordering deterministic:
|
||||
// - pure ASCII printable: send via Portal keysym
|
||||
// - any non-ASCII present (including mixed ASCII/non-ASCII): send whole
|
||||
// sequence via clipboard as one atomic paste
|
||||
let ascii_only = s.chars().all(|c| {
|
||||
let keysym = char_to_keysym(c);
|
||||
can_input_via_keysym(c, keysym)
|
||||
});
|
||||
if !ascii_only {
|
||||
input_text_via_clipboard(s, self.conn.clone(), &self.session);
|
||||
return;
|
||||
}
|
||||
|
||||
for c in s.chars() {
|
||||
let keysym = char_to_keysym(c);
|
||||
// ASCII characters: use keysym
|
||||
|
|
@ -128,9 +145,6 @@ pub mod client {
|
|||
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
|
||||
log::error!("Failed to send keysym up: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
// Non-ASCII: use clipboard
|
||||
input_text_via_clipboard(&c.to_string(), self.conn.clone(), &self.session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -167,8 +181,7 @@ pub mod client {
|
|||
// ASCII characters: send keysym up if we also sent it on key_down
|
||||
let keysym = char_to_keysym(chr);
|
||||
if can_input_via_keysym(chr, keysym) {
|
||||
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session)
|
||||
{
|
||||
if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) {
|
||||
log::error!("Failed to send keysym up: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue