mirror of
https://github.com/rustdesk/rustdesk
synced 2026-04-21 13:27:19 +00:00
Merge 3801fa7603 into e8a1b7fe21
This commit is contained in:
commit
0245b8f848
4 changed files with 204 additions and 31 deletions
|
|
@ -15,6 +15,7 @@ import 'package:get/get.dart';
|
|||
import '../../models/model.dart';
|
||||
import '../../models/platform_model.dart';
|
||||
import '../../models/state_model.dart';
|
||||
import 'ios_caps_lock_state_tracker.dart';
|
||||
import 'relative_mouse_model.dart';
|
||||
import '../common.dart';
|
||||
import '../consts.dart';
|
||||
|
|
@ -335,6 +336,7 @@ class InputModel {
|
|||
var ctrl = false;
|
||||
var alt = false;
|
||||
var command = false;
|
||||
final _iosCapsLockTracker = IosCapsLockStateTracker();
|
||||
|
||||
final ToReleaseRawKeys toReleaseRawKeys = ToReleaseRawKeys();
|
||||
final ToReleaseKeys toReleaseKeys = ToReleaseKeys();
|
||||
|
|
@ -441,39 +443,42 @@ class InputModel {
|
|||
// incorrect CapsLock state on iOS.
|
||||
bool _getIosCapsFromCharacter(KeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(
|
||||
ch, HardwareKeyboard.instance.isShiftPressed);
|
||||
return _getIosCapsLockState(
|
||||
character: e.character,
|
||||
shiftPressed: HardwareKeyboard.instance.isShiftPressed,
|
||||
logicalKey: e.logicalKey,
|
||||
isKeyDown: e is KeyDownEvent || e is KeyRepeatEvent,
|
||||
);
|
||||
}
|
||||
|
||||
// RawKeyEvent version of _getIosCapsFromCharacter.
|
||||
bool _getIosCapsFromRawCharacter(RawKeyEvent e) {
|
||||
if (!isIOS) return false;
|
||||
final ch = e.character;
|
||||
return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed);
|
||||
return _getIosCapsLockState(
|
||||
character: e.character,
|
||||
shiftPressed: e.isShiftPressed,
|
||||
logicalKey: e.logicalKey,
|
||||
isKeyDown: e is RawKeyDownEvent,
|
||||
);
|
||||
}
|
||||
|
||||
// Shared implementation for inferring CapsLock state from character.
|
||||
// Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É).
|
||||
//
|
||||
// Limitations:
|
||||
// 1. This inference assumes the client and server use the same keyboard layout.
|
||||
// If layouts differ (e.g., client uses EN, server uses DE), the character output
|
||||
// may not match expectations. For example, ';' on EN layout maps to 'ö' on DE
|
||||
// layout, making it impossible to correctly infer CapsLock state from the
|
||||
// character alone.
|
||||
// 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it
|
||||
// produces lowercase). This method cannot handle that case correctly.
|
||||
bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) {
|
||||
if (ch == null || ch.length != 1) return false;
|
||||
// Use Dart's built-in Unicode-aware case detection
|
||||
final upper = ch.toUpperCase();
|
||||
final lower = ch.toLowerCase();
|
||||
final isUpper = upper == ch && lower != ch;
|
||||
final isLower = lower == ch && upper != ch;
|
||||
// Skip non-letter characters (e.g., numbers, symbols, CJK characters without case)
|
||||
if (!isUpper && !isLower) return false;
|
||||
return isUpper != shiftPressed;
|
||||
bool _getIosCapsLockState({
|
||||
required String? character,
|
||||
required bool shiftPressed,
|
||||
required LogicalKeyboardKey logicalKey,
|
||||
required bool isKeyDown,
|
||||
}) {
|
||||
if (!isIOS) return false;
|
||||
// Flutter's reported lock state is unreliable on iOS. Keep a cached
|
||||
// value and update it from explicit CapsLock presses or inferable
|
||||
// character output, then reuse that cached state for key-up and
|
||||
// non-character events.
|
||||
return _iosCapsLockTracker.update(
|
||||
character: character,
|
||||
shiftPressed: shiftPressed,
|
||||
logicalKey: logicalKey,
|
||||
isKeyDown: isKeyDown,
|
||||
);
|
||||
}
|
||||
|
||||
int _buildLockModes(bool iosCapsLock) {
|
||||
|
|
@ -636,7 +641,7 @@ class InputModel {
|
|||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && e is RawKeyDownEvent) {
|
||||
if (isIOS) {
|
||||
iosCapsLock = _getIosCapsFromRawCharacter(e);
|
||||
}
|
||||
|
||||
|
|
@ -713,7 +718,7 @@ class InputModel {
|
|||
}
|
||||
|
||||
bool iosCapsLock = false;
|
||||
if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) {
|
||||
if (isIOS) {
|
||||
iosCapsLock = _getIosCapsFromCharacter(e);
|
||||
}
|
||||
|
||||
|
|
@ -952,9 +957,12 @@ class InputModel {
|
|||
.encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()})));
|
||||
}
|
||||
|
||||
/// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command].
|
||||
/// Reset local key state, including modifiers and cached iOS lock state.
|
||||
void resetModifiers() {
|
||||
shift = ctrl = alt = command = false;
|
||||
if (isIOS) {
|
||||
_iosCapsLockTracker.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/// Modify the given modifier map [evt] based on current modifier key status.
|
||||
|
|
|
|||
42
flutter/lib/models/ios_caps_lock_state_tracker.dart
Normal file
42
flutter/lib/models/ios_caps_lock_state_tracker.dart
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
class IosCapsLockStateTracker {
|
||||
bool _capsLock = false;
|
||||
|
||||
bool get value => _capsLock;
|
||||
|
||||
void reset() {
|
||||
_capsLock = false;
|
||||
}
|
||||
|
||||
bool update({
|
||||
required String? character,
|
||||
required bool shiftPressed,
|
||||
required LogicalKeyboardKey logicalKey,
|
||||
required bool isKeyDown,
|
||||
}) {
|
||||
if (isKeyDown && logicalKey == LogicalKeyboardKey.capsLock) {
|
||||
_capsLock = !_capsLock;
|
||||
return _capsLock;
|
||||
}
|
||||
if (!isKeyDown) {
|
||||
return _capsLock;
|
||||
}
|
||||
final inferred = inferFromCharacter(character, shiftPressed);
|
||||
if (inferred != null) {
|
||||
_capsLock = inferred;
|
||||
}
|
||||
return _capsLock;
|
||||
}
|
||||
|
||||
static bool? inferFromCharacter(String? character, bool shiftPressed) {
|
||||
if (shiftPressed) return null;
|
||||
if (character == null || character.length != 1) return null;
|
||||
final upper = character.toUpperCase();
|
||||
final lower = character.toLowerCase();
|
||||
final isUpper = upper == character && lower != character;
|
||||
final isLower = lower == character && upper != character;
|
||||
if (!isUpper && !isLower) return null;
|
||||
return isUpper;
|
||||
}
|
||||
}
|
||||
|
|
@ -113,8 +113,8 @@ dependencies:
|
|||
|
||||
dev_dependencies:
|
||||
icons_launcher: ^2.0.4
|
||||
#flutter_test:
|
||||
#sdk: flutter
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.4.6
|
||||
freezed: ^2.4.2
|
||||
flutter_lints: ^2.0.2
|
||||
|
|
|
|||
123
flutter/test/models/input_model_ios_capslock_test.dart
Normal file
123
flutter/test/models/input_model_ios_capslock_test.dart
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_hbb/models/ios_caps_lock_state_tracker.dart';
|
||||
|
||||
void main() {
|
||||
group('IosCapsLockStateTracker', () {
|
||||
test('preserves cached caps lock state for non-character events', () {
|
||||
final tracker = IosCapsLockStateTracker();
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.capsLock,
|
||||
isKeyDown: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: 'A',
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
isKeyDown: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
isKeyDown: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not change cached caps lock state on key up', () {
|
||||
final tracker = IosCapsLockStateTracker();
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.capsLock,
|
||||
isKeyDown: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: 'a',
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
isKeyDown: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not clear cached caps lock state when shift is pressed', () {
|
||||
final tracker = IosCapsLockStateTracker();
|
||||
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.capsLock,
|
||||
isKeyDown: true,
|
||||
);
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: 'A',
|
||||
shiftPressed: true,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
isKeyDown: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
isKeyDown: false,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('reset clears cached caps lock state', () {
|
||||
final tracker = IosCapsLockStateTracker();
|
||||
|
||||
expect(
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.capsLock,
|
||||
isKeyDown: true,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
|
||||
tracker.reset();
|
||||
|
||||
expect(tracker.value, isFalse);
|
||||
expect(
|
||||
tracker.update(
|
||||
character: null,
|
||||
shiftPressed: false,
|
||||
logicalKey: LogicalKeyboardKey.keyA,
|
||||
isKeyDown: false,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue