mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-14 08:21:02 +03: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:async';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.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/input_modifier_utils.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_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/models/terminal_model.dart';
|
||||||
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
import 'package:flutter_hbb/mobile/terminal_keyboard_utils.dart';
|
||||||
import 'package:flutter_hbb/web/dummy.dart'
|
import 'package:flutter_hbb/web/dummy.dart'
|
||||||
@@ -190,6 +192,7 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
KeyEventResult _handleTerminalKeyEvent(FocusNode _, KeyEvent event) {
|
||||||
final hardwareKeyboard = HardwareKeyboard.instance;
|
final hardwareKeyboard = HardwareKeyboard.instance;
|
||||||
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
final shouldPaste = shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: defaultTargetPlatform,
|
||||||
logicalKey: event.logicalKey,
|
logicalKey: event.logicalKey,
|
||||||
isKeyDown: event is KeyDownEvent,
|
isKeyDown: event is KeyDownEvent,
|
||||||
isKeyRepeat: event is KeyRepeatEvent,
|
isKeyRepeat: event is KeyRepeatEvent,
|
||||||
@@ -244,7 +247,12 @@ class _TerminalPageState extends State<TerminalPage>
|
|||||||
//
|
//
|
||||||
// Android works fine without this workaround.
|
// Android works fine without this workaround.
|
||||||
deleteDetection: isIOS,
|
deleteDetection: isIOS,
|
||||||
onKeyEvent: _handleTerminalKeyEvent,
|
shortcuts: platformTerminalShortcuts(),
|
||||||
|
onKeyEvent: terminalCopyHandler(
|
||||||
|
_terminalModel.terminal,
|
||||||
|
_terminalModel.terminalController,
|
||||||
|
fallback: _handleTerminalKeyEvent,
|
||||||
|
),
|
||||||
padding: _calculatePadding(heightPx),
|
padding: _calculatePadding(heightPx),
|
||||||
onSecondaryTapDown: (details, offset) async {
|
onSecondaryTapDown: (details, offset) async {
|
||||||
final selection = _terminalModel.terminalController.selection;
|
final selection = _terminalModel.terminalController.selection;
|
||||||
|
|||||||
@@ -117,10 +117,11 @@ String prepareTerminalInputPayload(
|
|||||||
|
|
||||||
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
/// Returns true when a hardware paste shortcut must bypass keyboard modifiers.
|
||||||
///
|
///
|
||||||
/// xterm already handles hardware Ctrl/Cmd+V correctly in the common case. Only
|
/// xterm already handles each platform's paste shortcut in the common case.
|
||||||
/// intercept while a virtual Ctrl/Alt lock is active, because xterm can emit a
|
/// Only intercept while a virtual Ctrl/Alt lock is active, because xterm can
|
||||||
/// one-character paste as normal text when bracketed paste mode is disabled.
|
/// emit a one-character paste as normal text when bracketed paste mode is off.
|
||||||
bool shouldHandleTerminalPasteShortcut({
|
bool shouldHandleTerminalPasteShortcut({
|
||||||
|
required TargetPlatform platform,
|
||||||
required LogicalKeyboardKey logicalKey,
|
required LogicalKeyboardKey logicalKey,
|
||||||
required bool isKeyDown,
|
required bool isKeyDown,
|
||||||
required bool isKeyRepeat,
|
required bool isKeyRepeat,
|
||||||
@@ -133,8 +134,18 @@ bool shouldHandleTerminalPasteShortcut({
|
|||||||
if (!modifierLockActive) return false;
|
if (!modifierLockActive) return false;
|
||||||
if (!isKeyDown && !isKeyRepeat) return false;
|
if (!isKeyDown && !isKeyRepeat) return false;
|
||||||
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
if (logicalKey != LogicalKeyboardKey.keyV) return false;
|
||||||
if (altPressed || shiftPressed) return false;
|
if (altPressed) return false;
|
||||||
return controlPressed != metaPressed;
|
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.
|
/// 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() {
|
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 {
|
return {
|
||||||
for (final entry in defaultTerminalShortcuts.entries)
|
for (final entry in defaultTerminalShortcuts.entries)
|
||||||
if (!_isControlVShortcut(entry.key)) entry.key: entry.value,
|
if (!_isControlShortcut(
|
||||||
_controlShiftVPasteShortcut:
|
entry.key,
|
||||||
const PasteTextIntent(SelectionChangedCause.keyboard),
|
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 is SingleActivator &&
|
||||||
shortcut.trigger == LogicalKeyboardKey.keyV &&
|
shortcut.trigger == key &&
|
||||||
shortcut.control &&
|
shortcut.control &&
|
||||||
!shortcut.shift &&
|
shortcut.shift == shift &&
|
||||||
!shortcut.alt &&
|
!shortcut.alt &&
|
||||||
!shortcut.meta;
|
!shortcut.meta;
|
||||||
|
|
||||||
FocusOnKeyEventCallback terminalCopyHandler(
|
FocusOnKeyEventCallback terminalCopyHandler(
|
||||||
Terminal terminal,
|
Terminal terminal,
|
||||||
TerminalController controller,
|
TerminalController controller, {
|
||||||
) =>
|
FocusOnKeyEventCallback? fallback,
|
||||||
(_, event) {
|
}) =>
|
||||||
if (!_isWindowsCopyShortcut(event)) return KeyEventResult.ignored;
|
(focusNode, event) {
|
||||||
final selection = controller.selection;
|
if (_isSelectionCopyShortcut(event)) {
|
||||||
if (selection == null || selection.isCollapsed) {
|
final selection = controller.selection;
|
||||||
return KeyEventResult.ignored;
|
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) {
|
return fallback?.call(focusNode, event) ?? KeyEventResult.ignored;
|
||||||
final text = terminal.buffer.getText(selection);
|
|
||||||
unawaited(writeTerminalClipboard(text));
|
|
||||||
}
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
bool _isWindowsCopyShortcut(KeyEvent event) {
|
bool _isSelectionCopyShortcut(KeyEvent event) {
|
||||||
final keyboard = HardwareKeyboard.instance;
|
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 is KeyDownEvent || event is KeyRepeatEvent) &&
|
||||||
event.logicalKey == LogicalKeyboardKey.keyC &&
|
event.logicalKey == LogicalKeyboardKey.keyC &&
|
||||||
keyboard.isControlPressed &&
|
keyboard.isControlPressed &&
|
||||||
|
|||||||
@@ -342,11 +342,43 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('shouldHandleTerminalPasteShortcut', () {
|
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(
|
test(
|
||||||
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
'keeps default xterm paste behavior when virtual modifiers are inactive',
|
||||||
() {
|
() {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -364,6 +396,7 @@ void main() {
|
|||||||
() {
|
() {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -377,6 +410,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.macOS,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -393,6 +427,7 @@ void main() {
|
|||||||
test('handles paste shortcut repeats while a virtual lock is active', () {
|
test('handles paste shortcut repeats while a virtual lock is active', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: false,
|
isKeyDown: false,
|
||||||
isKeyRepeat: true,
|
isKeyRepeat: true,
|
||||||
@@ -409,6 +444,7 @@ void main() {
|
|||||||
test('ignores key-up and unmodified V events', () {
|
test('ignores key-up and unmodified V events', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: false,
|
isKeyDown: false,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -422,6 +458,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -444,6 +481,7 @@ void main() {
|
|||||||
]) {
|
]) {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyV,
|
logicalKey: LogicalKeyboardKey.keyV,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
@@ -461,6 +499,7 @@ void main() {
|
|||||||
test('ignores non-V key events', () {
|
test('ignores non-V key events', () {
|
||||||
expect(
|
expect(
|
||||||
shouldHandleTerminalPasteShortcut(
|
shouldHandleTerminalPasteShortcut(
|
||||||
|
platform: TargetPlatform.windows,
|
||||||
logicalKey: LogicalKeyboardKey.keyC,
|
logicalKey: LogicalKeyboardKey.keyC,
|
||||||
isKeyDown: true,
|
isKeyDown: true,
|
||||||
isKeyRepeat: false,
|
isKeyRepeat: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user