blob: 9498406381eb24fd64c55de073f3cb73bf770c4a [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'common.dart';
import 'layout_types.dart';
// Maps all mandatory goals from the character to eventScanCode.
//
// Mandatory goals are all the alnum keys. These keys must be assigned at the
// end of layout planning.
final Map<String, String> _kMandatoryGoalsByChar = Map<String, String>.fromEntries(
kLayoutGoals
.entries
.where((MapEntry<String, String> entry) => isLetter(entry.value.codeUnitAt(0)))
.map((MapEntry<String, String?> entry) => MapEntry<String, String>(entry.value!, entry.key))
);
/// Plan a layout into a map from eventCode to logical key.
///
/// If a eventCode does not exist in this map, then this event's logical key
/// should be derived on the fly.
///
/// This function defines the benchmark planning algorithm if there were a way
/// to know the current keyboard layout.
Map<String, int> planLayout(Map<String, LayoutEntry> entries) {
// The logical key is derived in the following rules:
//
// 1. If any clue (the four possible printables) of the key is a mandatory
// goal (alnum), then the goal is the logical key.
// 2. If a mandatory goal is not assigned in the way of #1, then it is
// assigned to the physical key as mapped in the US layout.
// 3. Derived on the fly from key, code, and keyCode.
//
// The map returned from this function contains the first two rules.
// Unresolved mandatory goals, mapped from printables to KeyboardEvent.code.
// This map will be modified during this function and thus is a clone.
final Map<String, String> mandatoryGoalsByChar = <String, String>{..._kMandatoryGoalsByChar};
// The result mapping from KeyboardEvent.code to logical key.
final Map<String, int> result = <String, int>{};
entries.forEach((String eventCode, LayoutEntry entry) {
for (final String printable in entry.printables) {
if (mandatoryGoalsByChar.containsKey(printable)) {
result[eventCode] = printable.codeUnitAt(0);
mandatoryGoalsByChar.remove(printable);
break;
}
}
});
// Ensure all mandatory goals are assigned.
mandatoryGoalsByChar.forEach((String character, String code) {
assert(!result.containsKey(code), 'Code $code conflicts.');
result[code] = character.codeUnitAt(0);
});
return result;
}
bool _isLetterOrMappedToKeyCode(int charCode) {
return isLetterChar(charCode) || charCode == kUseKeyCode;
}
/// Plan all layouts, and summarize them into a huge table of EventCode ->
/// EventKey -> logicalKey.
///
/// The resulting logicalKey can also be kUseKeyCode.
///
/// If a eventCode does not exist in this map, then this event's logical key
/// should be derived on the fly.
///
/// Entries that can be derived using heuristics are omitted.
Map<String, Map<String, int>> combineLayouts(Iterable<Layout> layouts) {
final Map<String, Map<String, int>> result = <String, Map<String, int>>{};
for (final Layout layout in layouts) {
planLayout(layout.entries).forEach((String eventCode, int logicalKey) {
final Map<String, int> codeMap = result.putIfAbsent(eventCode, () => <String, int>{});
final LayoutEntry entry = layout.entries[eventCode]!;
for (final String eventKey in entry.printables) {
if (eventKey.isEmpty) {
continue;
}
// Found conflict. Assert that all such cases can be solved with
// keyCode.
if (codeMap.containsKey(eventKey) && codeMap[eventKey] != logicalKey) {
assert(isLetterChar(logicalKey));
assert(_isLetterOrMappedToKeyCode(codeMap[eventKey]!), '$eventCode, $eventKey, ${codeMap[eventKey]!}');
codeMap[eventKey] = kUseKeyCode;
} else {
codeMap[eventKey] = logicalKey;
}
}
});
}
// Remove mapping results that can be derived using heuristics.
result.removeWhere((String eventCode, Map<String, int> codeMap) {
codeMap.removeWhere((String eventKey, int logicalKey) =>
heuristicMapper(eventCode, eventKey) == logicalKey,
);
return codeMap.isEmpty;
});
return result;
}