This commit is contained in:
s1korrrr 2026-04-20 20:53:33 +02:00 committed by GitHub
commit 0245b8f848
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 204 additions and 31 deletions

View file

@ -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.

View 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;
}
}

View file

@ -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

View 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,
);
});
});
}