mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-08-28 09:06:56 +00:00
fix(flutter): align terminal shortcuts with platform conventions (#15970)
* fix(flutter): align terminal shortcuts with platform conventions Signed-off-by: fufesou <linlong1266@gmail.com> * fix(flutter): handle Linux terminal paste with modifier locks Detect platform-specific paste shortcuts so Ctrl+Shift+V bypasses virtual Ctrl/Alt modifiers on Linux. Add regression coverage. Signed-off-by: fufesou <linlong1266@gmail.com> --------- Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -8,6 +9,7 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
||||
import 'package:flutter_hbb/models/input_modifier_utils.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/models/terminal_copy_shortcut.dart';
|
||||
import 'package:flutter_hbb/models/terminal_model.dart';
|
||||
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
||||
import 'package:flutter_hbb/web/dummy.dart'
|
||||
@@ -190,6 +192,7 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
||||
final hardwareKeyboard = HardwareKeyboard.instance;
|
||||
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
||||
platform: defaultTargetPlatform,
|
||||
logicalKey: event.logicalKey,
|
||||
isKeyDown: event is KeyDownEvent,
|
||||
isKeyRepeat: event is KeyRepeatEvent,
|
||||
@@ -244,7 +247,12 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
//
|
||||
// Android works fine without this workaround.
|
||||
deleteDetection: isIOS,
|
||||
onKeyEvent: _handleTerminalKeyEvent,
|
||||
shortcuts: platformTerminalShortcuts(),
|
||||
onKeyEvent: terminalCopyHandler(
|
||||
_terminalModel.terminal,
|
||||
_terminalModel.terminalController,
|
||||
fallback: _handleTerminalKeyEvent,
|
||||
),
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
|
||||
@@ -117,10 +117,11 @@ String prepareTerminalInputPayload(
|
||||
|
||||
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
||||
///
|
||||
/// xterm already handles hardware Ctrl/Cmd+V correctly in the common case. Only
|
||||
/// intercept while a virtual Ctrl/Alt lock is active, because xterm can emit a
|
||||
/// one-character paste as normal text when bracketed paste mode is disabled.
|
||||
/// xterm already handles each platform's paste shortcut in the common case.
|
||||
/// Only intercept while a virtual Ctrl/Alt lock is active, because xterm can
|
||||
/// emit a one-character paste as normal text when bracketed paste mode is off.
|
||||
bool shouldHandleTerminalPasteShortcut({
|
||||
required TargetPlatform platform,
|
||||
required LogicalKeyboardKey logicalKey,
|
||||
required bool isKeyDown,
|
||||
required bool isKeyRepeat,
|
||||
@@ -133,8 +134,18 @@ bool shouldHandleTerminalPasteShortcut({
|
||||
if (!modifierLockActive) return false;
|
||||
if (!isKeyDown && !isKeyRepeat) return false;
|
||||
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
||||
if (altPressed || shiftPressed) return false;
|
||||
return controlPressed != metaPressed;
|
||||
if (altPressed) return false;
|
||||
switch (platform) {
|
||||
case TargetPlatform.linux:
|
||||
return controlPressed && !metaPressed && shiftPressed;
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.macOS:
|
||||
return !controlPressed && metaPressed && !shiftPressed;
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.fuchsia:
|
||||
case TargetPlatform.windows:
|
||||
return controlPressed && !metaPressed && !shiftPressed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true when collapsing Row3 should also clear hidden modifier state.
|
||||
|
||||
@@ -20,43 +20,68 @@ Future<void> writeTerminalClipboard(String text) async {
|
||||
}
|
||||
|
||||
Map<ShortcutActivator, Intent>? platformTerminalShortcuts() {
|
||||
if (defaultTargetPlatform != TargetPlatform.linux) return null;
|
||||
final platform = defaultTargetPlatform;
|
||||
if (platform == TargetPlatform.linux) {
|
||||
return {
|
||||
for (final entry in defaultTerminalShortcuts.entries)
|
||||
if (!_isControlShortcut(entry.key, LogicalKeyboardKey.keyV))
|
||||
entry.key: entry.value,
|
||||
_controlShiftVPasteShortcut:
|
||||
const PasteTextIntent(SelectionChangedCause.keyboard),
|
||||
};
|
||||
}
|
||||
if (platform != TargetPlatform.windows &&
|
||||
platform != TargetPlatform.android) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
for (final entry in defaultTerminalShortcuts.entries)
|
||||
if (!_isControlVShortcut(entry.key)) entry.key: entry.value,
|
||||
_controlShiftVPasteShortcut:
|
||||
const PasteTextIntent(SelectionChangedCause.keyboard),
|
||||
if (!_isControlShortcut(
|
||||
entry.key,
|
||||
LogicalKeyboardKey.keyC,
|
||||
shift: true,
|
||||
))
|
||||
entry.key: entry.value,
|
||||
};
|
||||
}
|
||||
|
||||
bool _isControlVShortcut(ShortcutActivator shortcut) =>
|
||||
bool _isControlShortcut(
|
||||
ShortcutActivator shortcut,
|
||||
LogicalKeyboardKey key, {
|
||||
bool shift = false,
|
||||
}) =>
|
||||
shortcut is SingleActivator &&
|
||||
shortcut.trigger == LogicalKeyboardKey.keyV &&
|
||||
shortcut.trigger == key &&
|
||||
shortcut.control &&
|
||||
!shortcut.shift &&
|
||||
shortcut.shift == shift &&
|
||||
!shortcut.alt &&
|
||||
!shortcut.meta;
|
||||
|
||||
FocusOnKeyEventCallback terminalCopyHandler(
|
||||
Terminal terminal,
|
||||
TerminalController controller,
|
||||
) =>
|
||||
(_, event) {
|
||||
if (!_isWindowsCopyShortcut(event)) return KeyEventResult.ignored;
|
||||
final selection = controller.selection;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
return KeyEventResult.ignored;
|
||||
TerminalController controller, {
|
||||
FocusOnKeyEventCallback? fallback,
|
||||
}) =>
|
||||
(focusNode, event) {
|
||||
if (_isSelectionCopyShortcut(event)) {
|
||||
final selection = controller.selection;
|
||||
if (selection != null && !selection.isCollapsed) {
|
||||
if (event is KeyDownEvent) {
|
||||
final text = terminal.buffer.getText(selection);
|
||||
unawaited(writeTerminalClipboard(text));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
final text = terminal.buffer.getText(selection);
|
||||
unawaited(writeTerminalClipboard(text));
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
return fallback?.call(focusNode, event) ?? KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
bool _isWindowsCopyShortcut(KeyEvent event) {
|
||||
bool _isSelectionCopyShortcut(KeyEvent event) {
|
||||
final keyboard = HardwareKeyboard.instance;
|
||||
return defaultTargetPlatform == TargetPlatform.windows &&
|
||||
final platform = defaultTargetPlatform;
|
||||
final usesControlCopy =
|
||||
platform == TargetPlatform.windows || platform == TargetPlatform.android;
|
||||
return usesControlCopy &&
|
||||
(event is KeyDownEvent || event is KeyRepeatEvent) &&
|
||||
event.logicalKey == LogicalKeyboardKey.keyC &&
|
||||
keyboard.isControlPressed &&
|
||||
|
||||
@@ -342,11 +342,43 @@ void main() {
|
||||
});
|
||||
|
||||
group('shouldHandleTerminalPasteShortcut', () {
|
||||
test('handles only Ctrl+Shift+V on Linux with a virtual lock', () {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.linux,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
controlPressed: true,
|
||||
metaPressed: false,
|
||||
altPressed: false,
|
||||
shiftPressed: true,
|
||||
modifierLockActive: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.linux,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
controlPressed: true,
|
||||
metaPressed: false,
|
||||
altPressed: false,
|
||||
shiftPressed: false,
|
||||
modifierLockActive: true,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
||||
() {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
@@ -364,6 +396,7 @@ void main() {
|
||||
() {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
@@ -377,6 +410,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.macOS,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
@@ -393,6 +427,7 @@ void main() {
|
||||
test('handles paste shortcut repeats while a virtual lock is active', () {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: false,
|
||||
isKeyRepeat: true,
|
||||
@@ -409,6 +444,7 @@ void main() {
|
||||
test('ignores key-up and unmodified V events', () {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: false,
|
||||
isKeyRepeat: false,
|
||||
@@ -422,6 +458,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
@@ -444,6 +481,7 @@ void main() {
|
||||
]) {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyV,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
@@ -461,6 +499,7 @@ void main() {
|
||||
test('ignores non-V key events', () {
|
||||
expect(
|
||||
shouldHandleTerminalPasteShortcut(
|
||||
platform: TargetPlatform.windows,
|
||||
logicalKey: LogicalKeyboardKey.keyC,
|
||||
isKeyDown: true,
|
||||
isKeyRepeat: false,
|
||||
|
||||
Reference in New Issue
Block a user