Auto-format Framework (#160545)
This auto-formats all *.dart files in the repository outside of the `engine` subdirectory and enforces that these files stay formatted with a presubmit check. **Reviewers:** Please carefully review all the commits except for the one titled "formatted". The "formatted" commit was auto-generated by running `dev/tools/format.sh -a -f`. The other commits were hand-crafted to prepare the repo for the formatting change. I recommend reviewing the commits one-by-one via the "Commits" tab and avoiding Github's "Files changed" tab as it will likely slow down your browser because of the size of this PR. --------- Co-authored-by: Kate Lovett <katelovett@google.com> Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8e0993eda8
commit
5491c8c146
@@ -17,25 +17,23 @@ import 'dart:io';
|
||||
import 'package:args/args.dart';
|
||||
import 'package:crypto/crypto.dart';
|
||||
|
||||
enum GitRevisionStrategy {
|
||||
mergeBase,
|
||||
head,
|
||||
}
|
||||
enum GitRevisionStrategy { mergeBase, head }
|
||||
|
||||
final RegExp _hashRegex = RegExp(r'^([a-fA-F0-9]+)');
|
||||
|
||||
final ArgParser parser = ArgParser()
|
||||
..addOption(
|
||||
'strategy',
|
||||
abbr: 's',
|
||||
allowed: <String>['head', 'mergeBase'],
|
||||
defaultsTo: 'head',
|
||||
allowedHelp: <String, String>{
|
||||
'head': 'hash from git HEAD',
|
||||
'mergeBase': 'hash from the merge-base of HEAD and upstream/master',
|
||||
},
|
||||
)
|
||||
..addFlag('help', abbr: 'h', negatable: false);
|
||||
final ArgParser parser =
|
||||
ArgParser()
|
||||
..addOption(
|
||||
'strategy',
|
||||
abbr: 's',
|
||||
allowed: <String>['head', 'mergeBase'],
|
||||
defaultsTo: 'head',
|
||||
allowedHelp: <String, String>{
|
||||
'head': 'hash from git HEAD',
|
||||
'mergeBase': 'hash from the merge-base of HEAD and upstream/master',
|
||||
},
|
||||
)
|
||||
..addFlag('help', abbr: 'h', negatable: false);
|
||||
|
||||
Never printHelp({String? error}) {
|
||||
final Stdout out = error != null ? stderr : stdout;
|
||||
@@ -65,14 +63,9 @@ Future<int> main(List<String> args) async {
|
||||
final String result;
|
||||
try {
|
||||
result = await engineHash(
|
||||
(List<String> command) => Process.run(
|
||||
command.first,
|
||||
command.sublist(1),
|
||||
stdoutEncoding: utf8,
|
||||
),
|
||||
revisionStrategy: GitRevisionStrategy.values.byName(
|
||||
arguments.option('strategy')!,
|
||||
),
|
||||
(List<String> command) =>
|
||||
Process.run(command.first, command.sublist(1), stdoutEncoding: utf8),
|
||||
revisionStrategy: GitRevisionStrategy.values.byName(arguments.option('strategy')!),
|
||||
);
|
||||
} catch (e) {
|
||||
stderr.writeln('Error calculating engine hash: $e');
|
||||
@@ -95,14 +88,12 @@ Future<String> engineHash(
|
||||
case GitRevisionStrategy.head:
|
||||
base = 'HEAD';
|
||||
case GitRevisionStrategy.mergeBase:
|
||||
final ProcessResult processResult = await runProcess(
|
||||
<String>[
|
||||
'git',
|
||||
'merge-base',
|
||||
'upstream/master',
|
||||
'HEAD',
|
||||
],
|
||||
);
|
||||
final ProcessResult processResult = await runProcess(<String>[
|
||||
'git',
|
||||
'merge-base',
|
||||
'upstream/master',
|
||||
'HEAD',
|
||||
]);
|
||||
|
||||
if (processResult.exitCode != 0) {
|
||||
throw '''
|
||||
@@ -110,8 +101,7 @@ Unable to find merge-base hash of the repository:
|
||||
${processResult.stderr}''';
|
||||
}
|
||||
|
||||
final Match? baseHash =
|
||||
_hashRegex.matchAsPrefix(processResult.stdout as String);
|
||||
final Match? baseHash = _hashRegex.matchAsPrefix(processResult.stdout as String);
|
||||
if (baseHash?.groupCount != 1) {
|
||||
throw '''
|
||||
Unable to parse merge-base hash of the repository
|
||||
@@ -124,9 +114,14 @@ ${processResult.stdout}''';
|
||||
// This is important for future filtering of files, but also do not include
|
||||
// the developer's changes / in flight PRs.
|
||||
// The presence `engine` and `DEPS` are signals that you live in a monorepo world.
|
||||
final ProcessResult processResult = await runProcess(
|
||||
<String>['git', 'ls-tree', '-r', base, 'engine', 'DEPS'],
|
||||
);
|
||||
final ProcessResult processResult = await runProcess(<String>[
|
||||
'git',
|
||||
'ls-tree',
|
||||
'-r',
|
||||
base,
|
||||
'engine',
|
||||
'DEPS',
|
||||
]);
|
||||
|
||||
if (processResult.exitCode != 0) {
|
||||
throw '''
|
||||
@@ -140,8 +135,7 @@ ${processResult.stderr}''';
|
||||
throw 'Not in a monorepo';
|
||||
}
|
||||
|
||||
final Iterable<String> treeLines =
|
||||
LineSplitter.split(processResult.stdout as String);
|
||||
final Iterable<String> treeLines = LineSplitter.split(processResult.stdout as String);
|
||||
|
||||
// We could call `git hash-object --stdin` which would just take the input, calculate the size,
|
||||
// and then sha1sum it like: `blob $size\0$string'. However, that can have different line endings.
|
||||
|
||||
@@ -20,7 +20,8 @@ void log(String message) {
|
||||
|
||||
const String _commitTimestampFormat = '--format=%cI';
|
||||
DateTime _parseTimestamp(String line) => DateTime.parse(line.trim());
|
||||
int _countLines(String output) => output.trim().split('/n').where((String line) => line.isNotEmpty).length;
|
||||
int _countLines(String output) =>
|
||||
output.trim().split('/n').where((String line) => line.isNotEmpty).length;
|
||||
|
||||
String findCommit({
|
||||
required String primaryRepoDirectory,
|
||||
@@ -32,20 +33,49 @@ String findCommit({
|
||||
final DateTime anchor;
|
||||
if (primaryBranch == primaryTrunk) {
|
||||
log('on $primaryTrunk, using last commit time');
|
||||
anchor = _parseTimestamp(git(primaryRepoDirectory, <String>['log', _commitTimestampFormat, '--max-count=1', primaryBranch, '--']));
|
||||
anchor = _parseTimestamp(
|
||||
git(primaryRepoDirectory, <String>[
|
||||
'log',
|
||||
_commitTimestampFormat,
|
||||
'--max-count=1',
|
||||
primaryBranch,
|
||||
'--',
|
||||
]),
|
||||
);
|
||||
} else {
|
||||
final String mergeBase = git(primaryRepoDirectory, <String>['merge-base', primaryBranch, primaryTrunk], allowFailure: true).trim();
|
||||
final String mergeBase =
|
||||
git(primaryRepoDirectory, <String>[
|
||||
'merge-base',
|
||||
primaryBranch,
|
||||
primaryTrunk,
|
||||
], allowFailure: true).trim();
|
||||
if (mergeBase.isEmpty) {
|
||||
throw StateError('Branch $primaryBranch does not seem to have a common history with trunk $primaryTrunk.');
|
||||
throw StateError(
|
||||
'Branch $primaryBranch does not seem to have a common history with trunk $primaryTrunk.',
|
||||
);
|
||||
}
|
||||
anchor = _parseTimestamp(git(primaryRepoDirectory, <String>['log', _commitTimestampFormat, '--max-count=1', mergeBase, '--']));
|
||||
anchor = _parseTimestamp(
|
||||
git(primaryRepoDirectory, <String>[
|
||||
'log',
|
||||
_commitTimestampFormat,
|
||||
'--max-count=1',
|
||||
mergeBase,
|
||||
'--',
|
||||
]),
|
||||
);
|
||||
if (debugLogging) {
|
||||
final int missingTrunkCommits = _countLines(git(primaryRepoDirectory, <String>['rev-list', primaryTrunk, '^$primaryBranch', '--']));
|
||||
final int extraCommits = _countLines(git(primaryRepoDirectory, <String>['rev-list', primaryBranch, '^$primaryTrunk', '--']));
|
||||
final int missingTrunkCommits = _countLines(
|
||||
git(primaryRepoDirectory, <String>['rev-list', primaryTrunk, '^$primaryBranch', '--']),
|
||||
);
|
||||
final int extraCommits = _countLines(
|
||||
git(primaryRepoDirectory, <String>['rev-list', primaryBranch, '^$primaryTrunk', '--']),
|
||||
);
|
||||
if (missingTrunkCommits == 0 && extraCommits == 0) {
|
||||
log('$primaryBranch is even with $primaryTrunk at $mergeBase');
|
||||
} else {
|
||||
log('$primaryBranch branched from $primaryTrunk $missingTrunkCommits commits ago, trunk has advanced by $extraCommits commits since then.');
|
||||
log(
|
||||
'$primaryBranch branched from $primaryTrunk $missingTrunkCommits commits ago, trunk has advanced by $extraCommits commits since then.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,7 +90,11 @@ String findCommit({
|
||||
}
|
||||
|
||||
String git(String workingDirectory, List<String> arguments, {bool allowFailure = false}) {
|
||||
final ProcessResult result = Process.runSync('git', arguments, workingDirectory: workingDirectory);
|
||||
final ProcessResult result = Process.runSync(
|
||||
'git',
|
||||
arguments,
|
||||
workingDirectory: workingDirectory,
|
||||
);
|
||||
if (!allowFailure && result.exitCode != 0 || '${result.stderr}'.isNotEmpty) {
|
||||
throw ProcessException('git', arguments, '${result.stdout}${result.stderr}', result.exitCode);
|
||||
}
|
||||
@@ -68,24 +102,29 @@ String git(String workingDirectory, List<String> arguments, {bool allowFailure =
|
||||
}
|
||||
|
||||
void main(List<String> arguments) {
|
||||
if (arguments.isEmpty || arguments.length != 4 || arguments.contains('--help') || arguments.contains('-h')) {
|
||||
if (arguments.isEmpty ||
|
||||
arguments.length != 4 ||
|
||||
arguments.contains('--help') ||
|
||||
arguments.contains('-h')) {
|
||||
print(
|
||||
'Usage: dart find_commit.dart <path-to-primary-repo> <primary-trunk> <path-to-secondary-repo> <secondary-branch>\n'
|
||||
'This script will find the commit in the secondary repo that was contemporary\n'
|
||||
'when the commit in the primary repo was created. If that commit is on a\n'
|
||||
"branch, then the date of the branch's last merge is used instead."
|
||||
"branch, then the date of the branch's last merge is used instead.",
|
||||
);
|
||||
} else {
|
||||
final String primaryRepo = arguments.first;
|
||||
final String primaryTrunk = arguments[1];
|
||||
final String secondaryRepo = arguments[2];
|
||||
final String secondaryBranch = arguments.last;
|
||||
print(findCommit(
|
||||
primaryRepoDirectory: primaryRepo,
|
||||
primaryBranch: git(primaryRepo, <String>['rev-parse', '--abbrev-ref', 'HEAD']).trim(),
|
||||
primaryTrunk: primaryTrunk,
|
||||
secondaryRepoDirectory: secondaryRepo,
|
||||
secondaryBranch: secondaryBranch,
|
||||
).trim());
|
||||
print(
|
||||
findCommit(
|
||||
primaryRepoDirectory: primaryRepo,
|
||||
primaryBranch: git(primaryRepo, <String>['rev-parse', '--abbrev-ref', 'HEAD']).trim(),
|
||||
primaryTrunk: primaryTrunk,
|
||||
secondaryRepoDirectory: secondaryRepo,
|
||||
secondaryBranch: secondaryBranch,
|
||||
).trim(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,18 @@ import 'package:process_runner/process_runner.dart';
|
||||
Future<int> main(List<String> arguments) async {
|
||||
final ArgParser parser = ArgParser();
|
||||
parser.addFlag('help', help: 'Print help.', abbr: 'h');
|
||||
parser.addFlag('fix',
|
||||
abbr: 'f',
|
||||
help: 'Instead of just checking for formatting errors, fix them in place.');
|
||||
parser.addFlag('all-files',
|
||||
abbr: 'a',
|
||||
help: 'Instead of just checking for formatting errors in changed files, '
|
||||
'check for them in all files.');
|
||||
parser.addFlag(
|
||||
'fix',
|
||||
abbr: 'f',
|
||||
help: 'Instead of just checking for formatting errors, fix them in place.',
|
||||
);
|
||||
parser.addFlag(
|
||||
'all-files',
|
||||
abbr: 'a',
|
||||
help:
|
||||
'Instead of just checking for formatting errors in changed files, '
|
||||
'check for them in all files.',
|
||||
);
|
||||
|
||||
late final ArgResults options;
|
||||
try {
|
||||
@@ -34,10 +39,12 @@ Future<int> main(List<String> arguments) async {
|
||||
final File script = File.fromUri(Platform.script).absolute;
|
||||
final Directory flutterRoot = script.parent.parent.parent.parent;
|
||||
|
||||
final bool result = (await DartFormatChecker(
|
||||
flutterRoot: flutterRoot,
|
||||
allFiles: options['all-files'] as bool,
|
||||
).check(fix: options['fix'] as bool)) == 0;
|
||||
final bool result =
|
||||
(await DartFormatChecker(
|
||||
flutterRoot: flutterRoot,
|
||||
allFiles: options['all-files'] as bool,
|
||||
).check(fix: options['fix'] as bool)) ==
|
||||
0;
|
||||
|
||||
exit(result ? 0 : 1);
|
||||
}
|
||||
@@ -49,12 +56,8 @@ void _usage(ArgParser parser, {int exitCode = 1}) {
|
||||
}
|
||||
|
||||
class DartFormatChecker {
|
||||
DartFormatChecker({
|
||||
required this.flutterRoot,
|
||||
required this.allFiles,
|
||||
}) : processRunner = ProcessRunner(
|
||||
defaultWorkingDirectory: flutterRoot,
|
||||
);
|
||||
DartFormatChecker({required this.flutterRoot, required this.allFiles})
|
||||
: processRunner = ProcessRunner(defaultWorkingDirectory: flutterRoot);
|
||||
|
||||
final Directory flutterRoot;
|
||||
final bool allFiles;
|
||||
@@ -67,10 +70,7 @@ class DartFormatChecker {
|
||||
allFiles: allFiles,
|
||||
baseGitRef: baseGitRef,
|
||||
);
|
||||
return _checkFormat(
|
||||
filesToCheck: filesToCheck,
|
||||
fix: fix,
|
||||
);
|
||||
return _checkFormat(filesToCheck: filesToCheck, fix: fix);
|
||||
}
|
||||
|
||||
Future<String> _getDiffBaseRevision() async {
|
||||
@@ -89,10 +89,12 @@ class DartFormatChecker {
|
||||
// This is the preferred command to use, but developer checkouts often do
|
||||
// not have a clear fork point, so we fall back to just the regular
|
||||
// merge-base in that case.
|
||||
result = await _runGit(
|
||||
<String>['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'],
|
||||
processRunner,
|
||||
);
|
||||
result = await _runGit(<String>[
|
||||
'merge-base',
|
||||
'--fork-point',
|
||||
'FETCH_HEAD',
|
||||
'HEAD',
|
||||
], processRunner);
|
||||
} on ProcessRunnerException {
|
||||
result = await _runGit(<String>['merge-base', 'FETCH_HEAD', 'HEAD'], processRunner);
|
||||
}
|
||||
@@ -100,14 +102,14 @@ class DartFormatChecker {
|
||||
}
|
||||
|
||||
Future<String> _runGit(
|
||||
List<String> args,
|
||||
ProcessRunner processRunner, {
|
||||
bool failOk = false,
|
||||
}) async {
|
||||
final ProcessRunnerResult result = await processRunner.runProcess(
|
||||
<String>['git', ...args],
|
||||
failOk: failOk,
|
||||
);
|
||||
List<String> args,
|
||||
ProcessRunner processRunner, {
|
||||
bool failOk = false,
|
||||
}) async {
|
||||
final ProcessRunnerResult result = await processRunner.runProcess(<String>[
|
||||
'git',
|
||||
...args,
|
||||
], failOk: failOk);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
@@ -118,11 +120,7 @@ class DartFormatChecker {
|
||||
}) async {
|
||||
String output;
|
||||
if (allFiles) {
|
||||
output = await _runGit(<String>[
|
||||
'ls-files',
|
||||
'--',
|
||||
...types,
|
||||
], processRunner);
|
||||
output = await _runGit(<String>['ls-files', '--', ...types], processRunner);
|
||||
} else {
|
||||
output = await _runGit(<String>[
|
||||
'diff',
|
||||
@@ -135,13 +133,13 @@ class DartFormatChecker {
|
||||
...types,
|
||||
], processRunner);
|
||||
}
|
||||
return output.split('\n').where((String line) => line.isNotEmpty).toList();
|
||||
return output
|
||||
.split('\n')
|
||||
.where((String line) => line.isNotEmpty && !line.startsWith('engine'))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<int> _checkFormat({
|
||||
required List<String> filesToCheck,
|
||||
required bool fix,
|
||||
}) async {
|
||||
Future<int> _checkFormat({required List<String> filesToCheck, required bool fix}) async {
|
||||
final List<String> cmd = <String>[
|
||||
path.join(flutterRoot.path, 'bin', 'dart'),
|
||||
'format',
|
||||
@@ -170,19 +168,16 @@ class DartFormatChecker {
|
||||
errorJobs.add(completedJob);
|
||||
} else if (completedJob.result.exitCode == 1) {
|
||||
diffJobs.add(
|
||||
WorkerJob(
|
||||
<String>[
|
||||
'git',
|
||||
'diff',
|
||||
'--no-index',
|
||||
'--no-color',
|
||||
'--ignore-cr-at-eol',
|
||||
'--',
|
||||
completedJob.command.last,
|
||||
'-',
|
||||
],
|
||||
stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw),
|
||||
),
|
||||
WorkerJob(<String>[
|
||||
'git',
|
||||
'diff',
|
||||
'--no-index',
|
||||
'--no-color',
|
||||
'--ignore-cr-at-eol',
|
||||
'--',
|
||||
completedJob.command.last,
|
||||
'-',
|
||||
], stdinRaw: _codeUnitsAsStream(completedJob.result.stdoutRaw)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -210,23 +205,31 @@ class DartFormatChecker {
|
||||
if (incorrect.isNotEmpty) {
|
||||
final bool plural = incorrect.length > 1;
|
||||
if (fix) {
|
||||
stdout.writeln('Fixing ${incorrect.length} dart file${plural ? 's' : ''}'
|
||||
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
||||
stdout.writeln(
|
||||
'Fixing ${incorrect.length} dart file${plural ? 's' : ''}'
|
||||
' which ${plural ? 'were' : 'was'} formatted incorrectly.',
|
||||
);
|
||||
} else {
|
||||
stderr.writeln('Found ${incorrect.length} Dart file${plural ? 's' : ''}'
|
||||
' which ${plural ? 'were' : 'was'} formatted incorrectly.');
|
||||
final String fileList = incorrect.map(
|
||||
(WorkerJob job) => job.command[job.command.length - 2]
|
||||
).join(' ');
|
||||
stderr.writeln(
|
||||
'Found ${incorrect.length} Dart file${plural ? 's' : ''}'
|
||||
' which ${plural ? 'were' : 'was'} formatted incorrectly.',
|
||||
);
|
||||
final String fileList = incorrect
|
||||
.map((WorkerJob job) => job.command[job.command.length - 2])
|
||||
.join(' ');
|
||||
stdout.writeln();
|
||||
stdout.writeln('To fix, run `dart format $fileList` or:');
|
||||
stdout.writeln();
|
||||
stdout.writeln('git apply <<DONE');
|
||||
for (final WorkerJob job in incorrect) {
|
||||
stdout.write(job.result.stdout
|
||||
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
|
||||
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
|
||||
.replaceFirst(RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'), '')
|
||||
stdout.write(
|
||||
job.result.stdout
|
||||
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
|
||||
.replaceFirst('b/-', 'b/${job.command[job.command.length - 2]}')
|
||||
.replaceFirst(
|
||||
RegExp('\\+Formatted \\d+ files? \\(\\d+ changed\\) in \\d+.\\d+ seconds.\n'),
|
||||
'',
|
||||
),
|
||||
);
|
||||
}
|
||||
stdout.writeln('DONE');
|
||||
@@ -244,7 +247,9 @@ class DartFormatChecker {
|
||||
void _printErrorJobs(List<WorkerJob> errorJobs) {
|
||||
if (errorJobs.isNotEmpty) {
|
||||
final bool plural = errorJobs.length > 1;
|
||||
stderr.writeln('The formatter failed to run on ${errorJobs.length} Dart file${plural ? 's' : ''}.');
|
||||
stderr.writeln(
|
||||
'The formatter failed to run on ${errorJobs.length} Dart file${plural ? 's' : ''}.',
|
||||
);
|
||||
stdout.writeln();
|
||||
for (final WorkerJob job in errorJobs) {
|
||||
stdout.writeln('--> ${job.command.last} produced the following error:');
|
||||
@@ -257,19 +262,20 @@ class DartFormatChecker {
|
||||
|
||||
ProcessPoolProgressReporter _namedReport(String name) {
|
||||
return (int total, int completed, int inProgress, int pending, int failed) {
|
||||
final String percent =
|
||||
total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
|
||||
final String percent = total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
|
||||
final String completedStr = completed.toString().padLeft(3);
|
||||
final String totalStr = total.toString().padRight(3);
|
||||
final String inProgressStr = inProgress.toString().padLeft(2);
|
||||
final String pendingStr = pending.toString().padLeft(3);
|
||||
final String failedStr = failed.toString().padLeft(3);
|
||||
|
||||
stdout.write('$name Jobs: $percent% done, '
|
||||
'$completedStr/$totalStr completed, '
|
||||
'$inProgressStr in progress, '
|
||||
'$pendingStr pending, '
|
||||
'$failedStr failed.${' ' * 20}\r');
|
||||
stdout.write(
|
||||
'$name Jobs: $percent% done, '
|
||||
'$completedStr/$totalStr completed, '
|
||||
'$inProgressStr in progress, '
|
||||
'$pendingStr pending, '
|
||||
'$failedStr failed.${' ' * 20}\r',
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,23 +18,27 @@ import 'package:file/local.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void main(List<String> arguments) {
|
||||
const String usageMessage = "If you don't wish to re-generate the "
|
||||
const String usageMessage =
|
||||
"If you don't wish to re-generate the "
|
||||
'settings.gradle, build.gradle, and gradle-wrapper.properties files,\n'
|
||||
'add the flag `--no-gradle-generation`.\n'
|
||||
'This tool automatically excludes a set of android subdirectories, '
|
||||
'defined at dev/tools/bin/config/lockfile_exclusion.yaml.\n'
|
||||
'To disable this behavior, run with `--no-exclusion`.\n';
|
||||
|
||||
final ArgParser argParser = ArgParser()
|
||||
..addFlag(
|
||||
'gradle-generation',
|
||||
help: 'Re-generate gradle files in each processed directory.',
|
||||
defaultsTo: true,
|
||||
)..addFlag(
|
||||
'exclusion',
|
||||
help: 'Run the script using the config file at ./configs/lockfile_exclusion.yaml to skip the specified subdirectories.',
|
||||
defaultsTo: true,
|
||||
);
|
||||
final ArgParser argParser =
|
||||
ArgParser()
|
||||
..addFlag(
|
||||
'gradle-generation',
|
||||
help: 'Re-generate gradle files in each processed directory.',
|
||||
defaultsTo: true,
|
||||
)
|
||||
..addFlag(
|
||||
'exclusion',
|
||||
help:
|
||||
'Run the script using the config file at ./configs/lockfile_exclusion.yaml to skip the specified subdirectories.',
|
||||
defaultsTo: true,
|
||||
);
|
||||
|
||||
ArgResults args;
|
||||
try {
|
||||
@@ -55,17 +59,16 @@ void main(List<String> arguments) {
|
||||
|
||||
const FileSystem fileSystem = LocalFileSystem();
|
||||
|
||||
final Directory repoRoot = (() {
|
||||
final String repoRootPath = exec(
|
||||
'git',
|
||||
const <String>['rev-parse', '--show-toplevel'],
|
||||
).trim();
|
||||
final Directory repoRoot = fileSystem.directory(repoRootPath);
|
||||
if (!repoRoot.existsSync()) {
|
||||
throw StateError("Expected $repoRoot to exist but it didn't!");
|
||||
}
|
||||
return repoRoot;
|
||||
})();
|
||||
final Directory repoRoot =
|
||||
(() {
|
||||
final String repoRootPath =
|
||||
exec('git', const <String>['rev-parse', '--show-toplevel']).trim();
|
||||
final Directory repoRoot = fileSystem.directory(repoRootPath);
|
||||
if (!repoRoot.existsSync()) {
|
||||
throw StateError("Expected $repoRoot to exist but it didn't!");
|
||||
}
|
||||
return repoRoot;
|
||||
})();
|
||||
|
||||
final Iterable<Directory> androidDirectories = discoverAndroidDirectories(repoRoot);
|
||||
|
||||
@@ -80,10 +83,9 @@ void main(List<String> arguments) {
|
||||
final Set<String> exclusionSet;
|
||||
if (useExclusion) {
|
||||
exclusionSet = HashSet<String>.from(
|
||||
(loadYaml(exclusionFile.readAsStringSync()) as YamlList)
|
||||
.toList()
|
||||
.cast<String>()
|
||||
.map((String s) => '${repoRoot.path}/$s')
|
||||
(loadYaml(exclusionFile.readAsStringSync()) as YamlList).toList().cast<String>().map(
|
||||
(String s) => '${repoRoot.path}/$s',
|
||||
),
|
||||
);
|
||||
print('Loaded exclusion file from ${exclusionFile.path}.');
|
||||
} else {
|
||||
@@ -97,7 +99,9 @@ void main(List<String> arguments) {
|
||||
}
|
||||
|
||||
if (exclusionSet.contains(androidDirectory.path)) {
|
||||
print('${androidDirectory.path} is included in the exclusion config file at ${exclusionFile.path} - skipping');
|
||||
print(
|
||||
'${androidDirectory.path} is included in the exclusion config file at ${exclusionFile.path} - skipping',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -117,7 +121,9 @@ void main(List<String> arguments) {
|
||||
} else if (androidDirectory.childFile('settings.gradle.kts').existsSync()) {
|
||||
settingsGradle = androidDirectory.childFile('settings.gradle.kts');
|
||||
} else {
|
||||
print('${androidDirectory.childFile('settings.gradle').path}(.kts) does not exist - skipping');
|
||||
print(
|
||||
'${androidDirectory.childFile('settings.gradle').path}(.kts) does not exist - skipping',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -145,15 +151,15 @@ void main(List<String> arguments) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (androidDirectory.parent.childFile('pubspec.yaml').readAsStringSync().contains('deferred-components')) {
|
||||
if (androidDirectory.parent
|
||||
.childFile('pubspec.yaml')
|
||||
.readAsStringSync()
|
||||
.contains('deferred-components')) {
|
||||
print('${rootBuildGradle.path} uses deferred components - skipping');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!androidDirectory.parent
|
||||
.childDirectory('lib')
|
||||
.childFile('main.dart')
|
||||
.existsSync()) {
|
||||
if (!androidDirectory.parent.childDirectory('lib').childFile('main.dart').existsSync()) {
|
||||
print('${rootBuildGradle.path} no main.dart under lib - skipping');
|
||||
continue;
|
||||
}
|
||||
@@ -180,43 +186,29 @@ void main(List<String> arguments) {
|
||||
final String appDirectory = androidDirectory.parent.absolute.path;
|
||||
|
||||
// Fetch pub dependencies.
|
||||
final String flutterPath = repoRoot
|
||||
.childDirectory('bin')
|
||||
.childFile('flutter')
|
||||
.path;
|
||||
final String flutterPath = repoRoot.childDirectory('bin').childFile('flutter').path;
|
||||
exec(flutterPath, <String>['pub', 'get'], workingDirectory: appDirectory);
|
||||
|
||||
// Verify that the Gradlew wrapper exists.
|
||||
final File gradleWrapper = androidDirectory.childFile('gradlew');
|
||||
// Generate Gradle wrapper if it doesn't exist.
|
||||
if (!gradleWrapper.existsSync()) {
|
||||
exec(
|
||||
flutterPath,
|
||||
<String>['build', 'apk', '--config-only'],
|
||||
workingDirectory: appDirectory,
|
||||
);
|
||||
exec(flutterPath, <String>['build', 'apk', '--config-only'], workingDirectory: appDirectory);
|
||||
}
|
||||
|
||||
// Generate lock files.
|
||||
exec(
|
||||
gradleWrapper.absolute.path,
|
||||
<String>[':generateLockfiles'],
|
||||
workingDirectory: androidDirectory.absolute.path,
|
||||
);
|
||||
exec(gradleWrapper.absolute.path, <String>[
|
||||
':generateLockfiles',
|
||||
], workingDirectory: androidDirectory.absolute.path);
|
||||
|
||||
print('Processed');
|
||||
}
|
||||
}
|
||||
|
||||
String exec(
|
||||
String cmd,
|
||||
List<String> args, {
|
||||
String? workingDirectory,
|
||||
}) {
|
||||
String exec(String cmd, List<String> args, {String? workingDirectory}) {
|
||||
final ProcessResult result = Process.runSync(cmd, args, workingDirectory: workingDirectory);
|
||||
if (result.exitCode != 0) {
|
||||
throw ProcessException(
|
||||
cmd, args, '${result.stdout}${result.stderr}', result.exitCode);
|
||||
throw ProcessException(cmd, args, '${result.stdout}${result.stderr}', result.exitCode);
|
||||
}
|
||||
return result.stdout as String;
|
||||
}
|
||||
@@ -311,7 +303,8 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
''';
|
||||
|
||||
Iterable<Directory> discoverAndroidDirectories(Directory repoRoot) {
|
||||
return repoRoot.listSync(recursive: true)
|
||||
return repoRoot
|
||||
.listSync(recursive: true)
|
||||
.whereType<Directory>()
|
||||
.where((FileSystemEntity entity) => entity.basename == 'android');
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ const String kPlatformIntegrationPackageName = 'platform_integration';
|
||||
/// Additional package dependencies that we want to have in the docs,
|
||||
/// but not actually depend on them.
|
||||
const Map<String, (String path, String version)> kFakeDependencies = <String, (String, String)>{
|
||||
'flutter_gpu': ('flutter_gpu/gpu.dart', '\n sdk: flutter'),
|
||||
};
|
||||
'flutter_gpu': ('flutter_gpu/gpu.dart', '\n sdk: flutter'),
|
||||
};
|
||||
|
||||
class PlatformDocsSection {
|
||||
const PlatformDocsSection({
|
||||
@@ -102,7 +102,11 @@ Future<void> main(List<String> arguments) async {
|
||||
// The place to find customization files and configuration files for docs
|
||||
// generation.
|
||||
final Directory docsRoot =
|
||||
FlutterInformation.instance.getFlutterRoot().childDirectory('dev').childDirectory('docs').absolute;
|
||||
FlutterInformation.instance
|
||||
.getFlutterRoot()
|
||||
.childDirectory('dev')
|
||||
.childDirectory('docs')
|
||||
.absolute;
|
||||
final ArgParser argParser = _createArgsParser(
|
||||
publishDefault: docsRoot.childDirectory('doc').path,
|
||||
);
|
||||
@@ -133,7 +137,10 @@ Future<void> main(List<String> arguments) async {
|
||||
);
|
||||
configurator.generateConfiguration();
|
||||
|
||||
final PlatformDocGenerator platformGenerator = PlatformDocGenerator(outputDir: publishRoot, filesystem: filesystem);
|
||||
final PlatformDocGenerator platformGenerator = PlatformDocGenerator(
|
||||
outputDir: publishRoot,
|
||||
filesystem: filesystem,
|
||||
);
|
||||
await platformGenerator.generatePlatformDocs();
|
||||
|
||||
final DartdocGenerator dartdocGenerator = DartdocGenerator(
|
||||
@@ -154,14 +161,27 @@ Future<void> main(List<String> arguments) async {
|
||||
ArgParser _createArgsParser({required String publishDefault}) {
|
||||
final ArgParser parser = ArgParser();
|
||||
parser.addFlag('help', abbr: 'h', negatable: false, help: 'Show command help.');
|
||||
parser.addFlag('verbose',
|
||||
defaultsTo: true,
|
||||
help: 'Whether to report all error messages (on) or attempt to '
|
||||
'filter out some known false positives (off). Shut this off '
|
||||
'locally if you want to address Flutter-specific issues.');
|
||||
parser.addFlag('json', help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.');
|
||||
parser.addFlag('validate-links', help: 'Display warnings for broken links generated by dartdoc (slow)');
|
||||
parser.addOption('output-dir', defaultsTo: publishDefault, help: 'Sets the output directory for the documentation.');
|
||||
parser.addFlag(
|
||||
'verbose',
|
||||
defaultsTo: true,
|
||||
help:
|
||||
'Whether to report all error messages (on) or attempt to '
|
||||
'filter out some known false positives (off). Shut this off '
|
||||
'locally if you want to address Flutter-specific issues.',
|
||||
);
|
||||
parser.addFlag(
|
||||
'json',
|
||||
help: 'Display json-formatted output from dartdoc and skip stdout/stderr prefixing.',
|
||||
);
|
||||
parser.addFlag(
|
||||
'validate-links',
|
||||
help: 'Display warnings for broken links generated by dartdoc (slow)',
|
||||
);
|
||||
parser.addOption(
|
||||
'output-dir',
|
||||
defaultsTo: publishDefault,
|
||||
help: 'Sets the output directory for the documentation.',
|
||||
);
|
||||
return parser;
|
||||
}
|
||||
|
||||
@@ -216,13 +236,16 @@ class Configurator {
|
||||
_createPageFooter(packageRoot, version);
|
||||
_copyCustomizations();
|
||||
_createSearchMetadata(
|
||||
docsRoot.childDirectory('lib').childFile('opensearch.xml'), publishRoot.childFile('opensearch.xml'));
|
||||
docsRoot.childDirectory('lib').childFile('opensearch.xml'),
|
||||
publishRoot.childFile('opensearch.xml'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> generateOfflineAssetsIfNeeded() async {
|
||||
// Only create the offline docs if we're running in a non-presubmit build:
|
||||
// it takes too long otherwise.
|
||||
if (platform.environment.containsKey('LUCI_CI') && (platform.environment['LUCI_PR'] ?? '').isEmpty) {
|
||||
if (platform.environment.containsKey('LUCI_CI') &&
|
||||
(platform.environment['LUCI_PR'] ?? '').isEmpty) {
|
||||
_createOfflineZipFile();
|
||||
await _createDocset();
|
||||
_moveOfflineIntoPlace();
|
||||
@@ -264,8 +287,7 @@ class Configurator {
|
||||
'environment:',
|
||||
" sdk: '>=3.2.0-0 <4.0.0'",
|
||||
'dependencies:',
|
||||
for (final String package in findPackageNames(filesystem))
|
||||
' $package:\n sdk: flutter',
|
||||
for (final String package in findPackageNames(filesystem)) ' $package:\n sdk: flutter',
|
||||
' $kPlatformIntegrationPackageName: 0.0.1',
|
||||
for (final String package in kFakeDependencies.keys)
|
||||
' $package: ${kFakeDependencies[package]!.$2}',
|
||||
@@ -329,12 +351,15 @@ class Configurator {
|
||||
// /foo/../foo/bar/baz won't compare as identical.
|
||||
if (path.canonicalize(source.absolute.path) != path.canonicalize(destination.absolute.path)) {
|
||||
source.copySync(destination.path);
|
||||
print('Copied ${path.canonicalize(source.absolute.path)} to ${path.canonicalize(destination.absolute.path)}');
|
||||
print(
|
||||
'Copied ${path.canonicalize(source.absolute.path)} to ${path.canonicalize(destination.absolute.path)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
final Directory assetsDir = filesystem.directory(publishRoot.childDirectory('assets'));
|
||||
final Directory assetSource = docsRoot.childDirectory('assets');
|
||||
if (path.canonicalize(assetSource.absolute.path) == path.canonicalize(assetsDir.absolute.path)) {
|
||||
if (path.canonicalize(assetSource.absolute.path) ==
|
||||
path.canonicalize(assetsDir.absolute.path)) {
|
||||
// Don't try and copy the directory over itself.
|
||||
return;
|
||||
}
|
||||
@@ -346,7 +371,9 @@ class Configurator {
|
||||
assetSource,
|
||||
assetsDir,
|
||||
onFileCopied: (File src, File dest) {
|
||||
print('Copied ${path.canonicalize(src.absolute.path)} to ${path.canonicalize(dest.absolute.path)}');
|
||||
print(
|
||||
'Copied ${path.canonicalize(src.absolute.path)} to ${path.canonicalize(dest.absolute.path)}',
|
||||
);
|
||||
},
|
||||
filesystem: filesystem,
|
||||
);
|
||||
@@ -378,17 +405,14 @@ class Configurator {
|
||||
// never get to see the logs. Thus, we run it in the background and tail the
|
||||
// logs only if it fails.
|
||||
final ProcessWrapper result = ProcessWrapper(
|
||||
await processManager.start(
|
||||
<String>[
|
||||
'dashing',
|
||||
'build',
|
||||
'--source',
|
||||
publishRoot.path,
|
||||
'--config',
|
||||
docsRoot.childFile('dashing.json').path,
|
||||
],
|
||||
workingDirectory: packageRoot.path,
|
||||
),
|
||||
await processManager.start(<String>[
|
||||
'dashing',
|
||||
'build',
|
||||
'--source',
|
||||
publishRoot.path,
|
||||
'--config',
|
||||
docsRoot.childFile('dashing.json').path,
|
||||
], workingDirectory: packageRoot.path),
|
||||
);
|
||||
final List<int> buffer = <int>[];
|
||||
result.stdout.listen(buffer.addAll);
|
||||
@@ -404,21 +428,28 @@ class Configurator {
|
||||
buffer.clear();
|
||||
|
||||
// Copy the favicon file to the output directory.
|
||||
final File faviconFile =
|
||||
publishRoot.childDirectory('flutter').childDirectory('static-assets').childFile('favicon.png');
|
||||
final File faviconFile = publishRoot
|
||||
.childDirectory('flutter')
|
||||
.childDirectory('static-assets')
|
||||
.childFile('favicon.png');
|
||||
final File iconFile = packageRoot.childDirectory('flutter.docset').childFile('icon.png');
|
||||
faviconFile
|
||||
..createSync(recursive: true)
|
||||
..copySync(iconFile.path);
|
||||
|
||||
// Post-process the dashing output.
|
||||
final File infoPlist =
|
||||
packageRoot.childDirectory('flutter.docset').childDirectory('Contents').childFile('Info.plist');
|
||||
final File infoPlist = packageRoot
|
||||
.childDirectory('flutter.docset')
|
||||
.childDirectory('Contents')
|
||||
.childFile('Info.plist');
|
||||
String contents = infoPlist.readAsStringSync();
|
||||
|
||||
// Since I didn't want to add the XML package as a dependency just for this,
|
||||
// I just used a regular expression to make this simple change.
|
||||
final RegExp findRe = RegExp(r'(\s*<key>DocSetPlatformFamily</key>\s*<string>)[^<]+(</string>)', multiLine: true);
|
||||
final RegExp findRe = RegExp(
|
||||
r'(\s*<key>DocSetPlatformFamily</key>\s*<string>)[^<]+(</string>)',
|
||||
multiLine: true,
|
||||
);
|
||||
contents = contents.replaceAllMapped(findRe, (Match match) {
|
||||
return '${match.group(1)}dartlang${match.group(2)}';
|
||||
});
|
||||
@@ -427,20 +458,32 @@ class Configurator {
|
||||
if (!offlineDir.existsSync()) {
|
||||
offlineDir.createSync(recursive: true);
|
||||
}
|
||||
tarDirectory(packageRoot, offlineDir.childFile('flutter.docset.tar.gz'), processManager: processManager);
|
||||
tarDirectory(
|
||||
packageRoot,
|
||||
offlineDir.childFile('flutter.docset.tar.gz'),
|
||||
processManager: processManager,
|
||||
);
|
||||
|
||||
// Write the Dash/Zeal XML feed file.
|
||||
final bool isStable = platform.environment['LUCI_BRANCH'] == 'stable';
|
||||
offlineDir.childFile('flutter.xml').writeAsStringSync('<entry>\n'
|
||||
' <version>${FlutterInformation.instance.getFlutterVersion()}</version>\n'
|
||||
' <url>https://${isStable ? '' : 'main-'}api.flutter.dev/offline/flutter.docset.tar.gz</url>\n'
|
||||
'</entry>\n');
|
||||
offlineDir
|
||||
.childFile('flutter.xml')
|
||||
.writeAsStringSync(
|
||||
'<entry>\n'
|
||||
' <version>${FlutterInformation.instance.getFlutterVersion()}</version>\n'
|
||||
' <url>https://${isStable ? '' : 'main-'}api.flutter.dev/offline/flutter.docset.tar.gz</url>\n'
|
||||
'</entry>\n',
|
||||
);
|
||||
}
|
||||
|
||||
// Creates the offline ZIP file containing all of the website HTML files.
|
||||
void _createOfflineZipFile() {
|
||||
print('${DateTime.now().toLocal()}: Creating offline docs archive.');
|
||||
zipDirectory(publishRoot, packageRoot.childFile('flutter.docs.zip'), processManager: processManager);
|
||||
zipDirectory(
|
||||
publishRoot,
|
||||
packageRoot.childFile('flutter.docs.zip'),
|
||||
processManager: processManager,
|
||||
);
|
||||
}
|
||||
|
||||
// Moves the generated offline archives into the publish directory so that
|
||||
@@ -448,7 +491,9 @@ class Configurator {
|
||||
void _moveOfflineIntoPlace() {
|
||||
print('${DateTime.now().toUtc()}: Moving offline docs into place.');
|
||||
final Directory offlineDir = publishRoot.childDirectory('offline')..createSync(recursive: true);
|
||||
packageRoot.childFile('flutter.docs.zip').renameSync(offlineDir.childFile('flutter.docs.zip').path);
|
||||
packageRoot
|
||||
.childFile('flutter.docs.zip')
|
||||
.renameSync(offlineDir.childFile('flutter.docs.zip').path);
|
||||
}
|
||||
|
||||
// Creates a robots.txt file that disallows indexing unless the branch is the
|
||||
@@ -522,13 +567,15 @@ class DartdocGenerator {
|
||||
}
|
||||
|
||||
// Run pub.
|
||||
ProcessWrapper process = ProcessWrapper(await runPubProcess(
|
||||
arguments: <String>['get'],
|
||||
workingDirectory: packageRoot,
|
||||
environment: pubEnvironment,
|
||||
filesystem: filesystem,
|
||||
processManager: processManager,
|
||||
));
|
||||
ProcessWrapper process = ProcessWrapper(
|
||||
await runPubProcess(
|
||||
arguments: <String>['get'],
|
||||
workingDirectory: packageRoot,
|
||||
environment: pubEnvironment,
|
||||
filesystem: filesystem,
|
||||
processManager: processManager,
|
||||
),
|
||||
);
|
||||
printStream(process.stdout, prefix: 'pub:stdout: ');
|
||||
printStream(process.stderr, prefix: 'pub:stderr: ');
|
||||
final int code = await process.done;
|
||||
@@ -540,20 +587,16 @@ class DartdocGenerator {
|
||||
|
||||
// Verify which version of the global activated packages we're using.
|
||||
final ProcessResult versionResults = processManager.runSync(
|
||||
<String>[
|
||||
FlutterInformation.instance.getFlutterBinaryPath().path,
|
||||
'pub',
|
||||
'global',
|
||||
'list',
|
||||
],
|
||||
<String>[FlutterInformation.instance.getFlutterBinaryPath().path, 'pub', 'global', 'list'],
|
||||
workingDirectory: packageRoot.path,
|
||||
environment: pubEnvironment,
|
||||
stdoutEncoding: utf8,
|
||||
);
|
||||
print('');
|
||||
final Iterable<RegExpMatch> versionMatches =
|
||||
RegExp(r'^(?<name>dartdoc) (?<version>[^\s]+)', multiLine: true)
|
||||
.allMatches(versionResults.stdout as String);
|
||||
final Iterable<RegExpMatch> versionMatches = RegExp(
|
||||
r'^(?<name>dartdoc) (?<version>[^\s]+)',
|
||||
multiLine: true,
|
||||
).allMatches(versionResults.stdout as String);
|
||||
for (final RegExpMatch match in versionMatches) {
|
||||
print('${match.namedGroup('name')} version: ${match.namedGroup('version')}');
|
||||
}
|
||||
@@ -658,17 +701,21 @@ class DartdocGenerator {
|
||||
];
|
||||
|
||||
String quote(String arg) => arg.contains(' ') ? "'$arg'" : arg;
|
||||
print('Executing: (cd "${packageRoot.path}" ; '
|
||||
'${FlutterInformation.instance.getFlutterBinaryPath().path} '
|
||||
'pub '
|
||||
'${dartdocArgs.map<String>(quote).join(' ')})');
|
||||
print(
|
||||
'Executing: (cd "${packageRoot.path}" ; '
|
||||
'${FlutterInformation.instance.getFlutterBinaryPath().path} '
|
||||
'pub '
|
||||
'${dartdocArgs.map<String>(quote).join(' ')})',
|
||||
);
|
||||
|
||||
process = ProcessWrapper(await runPubProcess(
|
||||
arguments: dartdocArgs,
|
||||
workingDirectory: packageRoot,
|
||||
environment: pubEnvironment,
|
||||
processManager: processManager,
|
||||
));
|
||||
process = ProcessWrapper(
|
||||
await runPubProcess(
|
||||
arguments: dartdocArgs,
|
||||
workingDirectory: packageRoot,
|
||||
environment: pubEnvironment,
|
||||
processManager: processManager,
|
||||
),
|
||||
);
|
||||
printStream(
|
||||
process.stdout,
|
||||
prefix: useJson ? '' : 'dartdoc:stdout: ',
|
||||
@@ -711,7 +758,8 @@ class DartdocGenerator {
|
||||
}
|
||||
} else {
|
||||
throw Exception(
|
||||
"Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file.");
|
||||
"Missing example code sanity test file ${file.path}. Either it didn't get published, or you might have to update the test to look at a different file.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,15 +773,19 @@ class DartdocGenerator {
|
||||
publishRoot.childDirectory('assets').childFile('overrides.css'),
|
||||
flutterDirectory.childDirectory('dart-io').childFile('File-class.html'),
|
||||
flutterDirectory.childDirectory('dart-ui').childFile('Canvas-class.html'),
|
||||
flutterDirectory.childDirectory('dart-ui').childDirectory('Canvas').childFile('drawRect.html'),
|
||||
flutterDirectory
|
||||
.childDirectory('dart-ui')
|
||||
.childDirectory('Canvas')
|
||||
.childFile('drawRect.html'),
|
||||
flutterDirectory
|
||||
.childDirectory('flutter_driver')
|
||||
.childDirectory('FlutterDriver')
|
||||
.childFile('FlutterDriver.connectedTo.html'),
|
||||
flutterDirectory.childDirectory('flutter_gpu').childFile('flutter_gpu-library.html'),
|
||||
flutterDirectory
|
||||
.childDirectory('flutter_gpu')
|
||||
.childFile('flutter_gpu-library.html'),
|
||||
flutterDirectory.childDirectory('flutter_test').childDirectory('WidgetTester').childFile('pumpWidget.html'),
|
||||
.childDirectory('flutter_test')
|
||||
.childDirectory('WidgetTester')
|
||||
.childFile('pumpWidget.html'),
|
||||
flutterDirectory.childDirectory('material').childFile('Material-class.html'),
|
||||
flutterDirectory.childDirectory('material').childFile('Tooltip-class.html'),
|
||||
widgetsDirectory.childFile('Widget-class.html'),
|
||||
@@ -745,7 +797,9 @@ class DartdocGenerator {
|
||||
void _sanityCheckDocs([Platform platform = const LocalPlatform()]) {
|
||||
for (final File canary in canaries) {
|
||||
if (!canary.existsSync()) {
|
||||
throw Exception('Missing "${canary.path}", which probably means the documentation failed to build correctly.');
|
||||
throw Exception(
|
||||
'Missing "${canary.path}", which probably means the documentation failed to build correctly.',
|
||||
);
|
||||
}
|
||||
}
|
||||
// Make sure at least one example of each kind includes source code.
|
||||
@@ -801,7 +855,10 @@ class DartdocGenerator {
|
||||
}
|
||||
|
||||
void _copyIndexToRootOfDocs() {
|
||||
publishRoot.childDirectory('flutter').childFile('index.html').copySync(publishRoot.childFile('index.html').path);
|
||||
publishRoot
|
||||
.childDirectory('flutter')
|
||||
.childFile('index.html')
|
||||
.copySync(publishRoot.childFile('index.html').path);
|
||||
}
|
||||
|
||||
void _changePackageToSdkInTitlebar() {
|
||||
@@ -844,8 +901,9 @@ class DartdocGenerator {
|
||||
final Directory snippetsDir = publishRoot.childDirectory('snippets');
|
||||
if (snippetsDir.existsSync()) {
|
||||
const JsonEncoder jsonEncoder = JsonEncoder.withIndent(' ');
|
||||
final Iterable<File> files =
|
||||
snippetsDir.listSync().whereType<File>().where((File file) => path.extension(file.path) == '.json');
|
||||
final Iterable<File> files = snippetsDir.listSync().whereType<File>().where(
|
||||
(File file) => path.extension(file.path) == '.json',
|
||||
);
|
||||
// Combine all the metadata into a single JSON array.
|
||||
final Iterable<String> fileContents = files.map((File file) => file.readAsStringSync());
|
||||
final List<dynamic> metadataObjects = fileContents.map<dynamic>(json.decode).toList();
|
||||
@@ -896,13 +954,20 @@ class PlatformDocGenerator {
|
||||
|
||||
// On failure print a short snipped from the body in case it's helpful.
|
||||
final int bodyLength = math.min(1024, response.body.length);
|
||||
stderr.writeln('Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}');
|
||||
stderr.writeln(
|
||||
'Response status code ${response.statusCode}. Body: ${response.body.substring(0, bodyLength)}',
|
||||
);
|
||||
sleep(const Duration(seconds: 1));
|
||||
}
|
||||
return responseBytes == null ? null : ZipDecoder().decodeBytes(responseBytes);
|
||||
}
|
||||
|
||||
Future<void> _extractDocs(String url, String name, PlatformDocsSection platform, Directory outputDir) async {
|
||||
Future<void> _extractDocs(
|
||||
String url,
|
||||
String name,
|
||||
PlatformDocsSection platform,
|
||||
Directory outputDir,
|
||||
) async {
|
||||
const int maxTries = 5;
|
||||
final Archive? archive = await _fetchArchive(url, maxTries);
|
||||
if (archive == null) {
|
||||
@@ -935,8 +1000,12 @@ class PlatformDocGenerator {
|
||||
/// specified, for each source/destination file pair.
|
||||
///
|
||||
/// Creates `destDir` if needed.
|
||||
void copyDirectorySync(Directory srcDir, Directory destDir,
|
||||
{void Function(File srcFile, File destFile)? onFileCopied, required FileSystem filesystem}) {
|
||||
void copyDirectorySync(
|
||||
Directory srcDir,
|
||||
Directory destDir, {
|
||||
void Function(File srcFile, File destFile)? onFileCopied,
|
||||
required FileSystem filesystem,
|
||||
}) {
|
||||
if (!srcDir.existsSync()) {
|
||||
throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy');
|
||||
}
|
||||
@@ -959,8 +1028,14 @@ void copyDirectorySync(Directory srcDir, Directory destDir,
|
||||
}
|
||||
}
|
||||
|
||||
void printStream(Stream<List<int>> stream, {String prefix = '', List<Pattern> filter = const <Pattern>[]}) {
|
||||
stream.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((String line) {
|
||||
void printStream(
|
||||
Stream<List<int>> stream, {
|
||||
String prefix = '',
|
||||
List<Pattern> filter = const <Pattern>[],
|
||||
}) {
|
||||
stream.transform<String>(utf8.decoder).transform<String>(const LineSplitter()).listen((
|
||||
String line,
|
||||
) {
|
||||
if (!filter.any((Pattern pattern) => line.contains(pattern))) {
|
||||
print('$prefix$line'.trim());
|
||||
}
|
||||
@@ -970,17 +1045,14 @@ void printStream(Stream<List<int>> stream, {String prefix = '', List<Pattern> fi
|
||||
void zipDirectory(Directory src, File output, {required ProcessManager processManager}) {
|
||||
// We would use the archive package to do this in one line, but it
|
||||
// is a lot slower, and doesn't do compression nearly as well.
|
||||
final ProcessResult zipProcess = processManager.runSync(
|
||||
<String>[
|
||||
'zip',
|
||||
'-r',
|
||||
'-9',
|
||||
'-q',
|
||||
output.path,
|
||||
'.',
|
||||
],
|
||||
workingDirectory: src.path,
|
||||
);
|
||||
final ProcessResult zipProcess = processManager.runSync(<String>[
|
||||
'zip',
|
||||
'-r',
|
||||
'-9',
|
||||
'-q',
|
||||
output.path,
|
||||
'.',
|
||||
], workingDirectory: src.path);
|
||||
|
||||
if (zipProcess.exitCode != 0) {
|
||||
print('Creating offline ZIP archive ${output.path} failed:');
|
||||
@@ -992,17 +1064,14 @@ void zipDirectory(Directory src, File output, {required ProcessManager processMa
|
||||
void tarDirectory(Directory src, File output, {required ProcessManager processManager}) {
|
||||
// We would use the archive package to do this in one line, but it
|
||||
// is a lot slower, and doesn't do compression nearly as well.
|
||||
final ProcessResult tarProcess = processManager.runSync(
|
||||
<String>[
|
||||
'tar',
|
||||
'cf',
|
||||
output.path,
|
||||
'--use-compress-program',
|
||||
'gzip --best',
|
||||
'flutter.docset',
|
||||
],
|
||||
workingDirectory: src.path,
|
||||
);
|
||||
final ProcessResult tarProcess = processManager.runSync(<String>[
|
||||
'tar',
|
||||
'cf',
|
||||
output.path,
|
||||
'--use-compress-program',
|
||||
'gzip --best',
|
||||
'flutter.docset',
|
||||
], workingDirectory: src.path);
|
||||
|
||||
if (tarProcess.exitCode != 0) {
|
||||
print('Creating a tarball ${output.path} failed:');
|
||||
@@ -1026,7 +1095,9 @@ Future<Process> runPubProcess({
|
||||
}
|
||||
|
||||
List<String> findPackageNames(FileSystem filesystem) {
|
||||
return findPackages(filesystem).map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
|
||||
return findPackages(
|
||||
filesystem,
|
||||
).map<String>((FileSystemEntity file) => path.basename(file.path)).toList();
|
||||
}
|
||||
|
||||
/// Finds all packages in the Flutter SDK
|
||||
@@ -1141,42 +1212,50 @@ class FlutterInformation {
|
||||
// that flutter command, otherwise use the first one in the PATH.
|
||||
String flutterCommand;
|
||||
if (platform.environment['FLUTTER_ROOT'] != null) {
|
||||
flutterCommand = filesystem
|
||||
.directory(platform.environment['FLUTTER_ROOT'])
|
||||
.childDirectory('bin')
|
||||
.childFile('flutter')
|
||||
.absolute
|
||||
.path;
|
||||
flutterCommand =
|
||||
filesystem
|
||||
.directory(platform.environment['FLUTTER_ROOT'])
|
||||
.childDirectory('bin')
|
||||
.childFile('flutter')
|
||||
.absolute
|
||||
.path;
|
||||
} else {
|
||||
flutterCommand = 'flutter';
|
||||
}
|
||||
ProcessResult result;
|
||||
try {
|
||||
result = processManager.runSync(
|
||||
<String>[flutterCommand, '--version', '--machine'],
|
||||
stdoutEncoding: utf8,
|
||||
);
|
||||
result = processManager.runSync(<String>[
|
||||
flutterCommand,
|
||||
'--version',
|
||||
'--machine',
|
||||
], stdoutEncoding: utf8);
|
||||
} on ProcessException catch (e) {
|
||||
throw FlutterInformationException(
|
||||
'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place the '
|
||||
'flutter command in your PATH.\n$e');
|
||||
'Unable to determine Flutter information. Either set FLUTTER_ROOT, or place the '
|
||||
'flutter command in your PATH.\n$e',
|
||||
);
|
||||
}
|
||||
if (result.exitCode != 0) {
|
||||
throw FlutterInformationException(
|
||||
'Unable to determine Flutter information, because of abnormal exit of flutter command.');
|
||||
'Unable to determine Flutter information, because of abnormal exit of flutter command.',
|
||||
);
|
||||
}
|
||||
// Strip out any non-JSON that might be printed along with the command
|
||||
// output.
|
||||
flutterVersionJson = (result.stdout as String)
|
||||
.replaceAll('Waiting for another flutter command to release the startup lock...', '');
|
||||
flutterVersionJson = (result.stdout as String).replaceAll(
|
||||
'Waiting for another flutter command to release the startup lock...',
|
||||
'',
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, dynamic> flutterVersion = json.decode(flutterVersionJson) as Map<String, dynamic>;
|
||||
final Map<String, dynamic> flutterVersion =
|
||||
json.decode(flutterVersionJson) as Map<String, dynamic>;
|
||||
if (flutterVersion['flutterRoot'] == null ||
|
||||
flutterVersion['frameworkVersion'] == null ||
|
||||
flutterVersion['dartSdkVersion'] == null) {
|
||||
throw FlutterInformationException(
|
||||
'Flutter command output has unexpected format, unable to determine flutter root location.');
|
||||
'Flutter command output has unexpected format, unable to determine flutter root location.',
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, Object> info = <String, Object>{};
|
||||
@@ -1184,17 +1263,23 @@ class FlutterInformation {
|
||||
info['flutterRoot'] = flutterRoot;
|
||||
info['frameworkVersion'] = Version.parse(flutterVersion['frameworkVersion'] as String);
|
||||
info['engineRevision'] = flutterVersion['engineRevision'] as String;
|
||||
final File engineRealm = flutterRoot.childDirectory('bin').childDirectory('internal').childFile('engine.realm');
|
||||
final File engineRealm = flutterRoot
|
||||
.childDirectory('bin')
|
||||
.childDirectory('internal')
|
||||
.childFile('engine.realm');
|
||||
info['engineRealm'] = engineRealm.existsSync() ? engineRealm.readAsStringSync().trim() : '';
|
||||
|
||||
final RegExpMatch? dartVersionRegex = RegExp(r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?')
|
||||
.firstMatch(flutterVersion['dartSdkVersion'] as String);
|
||||
final RegExpMatch? dartVersionRegex = RegExp(
|
||||
r'(?<base>[\d.]+)(?:\s+\(build (?<detail>[-.\w]+)\))?',
|
||||
).firstMatch(flutterVersion['dartSdkVersion'] as String);
|
||||
if (dartVersionRegex == null) {
|
||||
throw FlutterInformationException(
|
||||
'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.');
|
||||
'Flutter command output has unexpected format, unable to parse dart SDK version ${flutterVersion['dartSdkVersion']}.',
|
||||
);
|
||||
}
|
||||
info['dartSdkVersion'] =
|
||||
Version.parse(dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!);
|
||||
info['dartSdkVersion'] = Version.parse(
|
||||
dartVersionRegex.namedGroup('detail') ?? dartVersionRegex.namedGroup('base')!,
|
||||
);
|
||||
|
||||
info['branchName'] = _getBranchName();
|
||||
info['flutterGitRevision'] = _getFlutterGitRevision();
|
||||
@@ -1212,13 +1297,19 @@ class FlutterInformation {
|
||||
if (luciBranch != null && luciBranch.trim().isNotEmpty) {
|
||||
return luciBranch.trim();
|
||||
}
|
||||
final ProcessResult gitResult = processManager.runSync(<String>['git', 'status', '-b', '--porcelain']);
|
||||
final ProcessResult gitResult = processManager.runSync(<String>[
|
||||
'git',
|
||||
'status',
|
||||
'-b',
|
||||
'--porcelain',
|
||||
]);
|
||||
if (gitResult.exitCode != 0) {
|
||||
throw 'git status exit with non-zero exit code: ${gitResult.exitCode}';
|
||||
}
|
||||
final RegExp gitBranchRegexp = RegExp(r'^## (.*)');
|
||||
final RegExpMatch? gitBranchMatch =
|
||||
gitBranchRegexp.firstMatch((gitResult.stdout as String).trim().split('\n').first);
|
||||
final RegExpMatch? gitBranchMatch = gitBranchRegexp.firstMatch(
|
||||
(gitResult.stdout as String).trim().split('\n').first,
|
||||
);
|
||||
return gitBranchMatch == null ? '' : gitBranchMatch.group(1)!.split('...').first;
|
||||
}
|
||||
|
||||
@@ -1232,6 +1323,8 @@ class FlutterInformation {
|
||||
}
|
||||
final String gitRevision = (gitResult.stdout as String).trim();
|
||||
|
||||
return gitRevision.length > kGitRevisionLength ? gitRevision.substring(0, kGitRevisionLength) : gitRevision;
|
||||
return gitRevision.length > kGitRevisionLength
|
||||
? gitRevision.substring(0, kGitRevisionLength)
|
||||
: gitRevision;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,9 @@ void checkForUnresolvedDirectives(Directory dartDocDir) {
|
||||
}
|
||||
|
||||
if (canaryLibraries.isNotEmpty) {
|
||||
throw Exception('Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.');
|
||||
throw Exception(
|
||||
'Did not find docs for the following libraries: ${canaryLibraries.join(', ')}.',
|
||||
);
|
||||
}
|
||||
if (canaryFiles.isNotEmpty) {
|
||||
throw Exception('Did not find docs for the following files: ${canaryFiles.join(', ')}.');
|
||||
@@ -88,8 +90,9 @@ void checkForUnresolvedDirectives(Directory dartDocDir) {
|
||||
|
||||
int _scanFile(File file) {
|
||||
assert(path.extension(file.path) == '.html');
|
||||
final Iterable<String> matches = _pattern.allMatches(file.readAsStringSync())
|
||||
.map((RegExpMatch m ) => m.group(0)!);
|
||||
final Iterable<String> matches = _pattern
|
||||
.allMatches(file.readAsStringSync())
|
||||
.map((RegExpMatch m) => m.group(0)!);
|
||||
|
||||
if (matches.isNotEmpty) {
|
||||
stderr.writeln('Found unresolved dartdoc directives in ${file.path}:');
|
||||
|
||||
@@ -28,13 +28,7 @@ FutureOr<dynamic> main() async {
|
||||
return;
|
||||
}
|
||||
final Directory flutterDir = _kFilesystem.directory(
|
||||
path.absolute(
|
||||
path.dirname(
|
||||
path.dirname(
|
||||
path.dirname(_kPlatform.script.toFilePath()),
|
||||
),
|
||||
),
|
||||
),
|
||||
path.absolute(path.dirname(path.dirname(path.dirname(_kPlatform.script.toFilePath())))),
|
||||
);
|
||||
final Directory apiDir = flutterDir.childDirectory('examples').childDirectory('api');
|
||||
final File integrationTest = await generateTest(apiDir);
|
||||
@@ -62,13 +56,14 @@ Future<void> runSmokeTests({
|
||||
required File integrationTest,
|
||||
required Directory apiDir,
|
||||
}) async {
|
||||
final File flutterExe =
|
||||
flutterDir.childDirectory('bin').childFile(_kPlatform.isWindows ? 'flutter.bat' : 'flutter');
|
||||
final File flutterExe = flutterDir
|
||||
.childDirectory('bin')
|
||||
.childFile(_kPlatform.isWindows ? 'flutter.bat' : 'flutter');
|
||||
final List<String> cmd = <String>[
|
||||
// If we're in a container with no X display, then use the virtual framebuffer.
|
||||
if (_kPlatform.isLinux &&
|
||||
(_kPlatform.environment['DISPLAY'] == null ||
|
||||
_kPlatform.environment['DISPLAY']!.isEmpty)) '/usr/bin/xvfb-run',
|
||||
(_kPlatform.environment['DISPLAY'] == null || _kPlatform.environment['DISPLAY']!.isEmpty))
|
||||
'/usr/bin/xvfb-run',
|
||||
flutterExe.absolute.path,
|
||||
'test',
|
||||
'--reporter=expanded',
|
||||
@@ -82,8 +77,8 @@ Future<void> runSmokeTests({
|
||||
// from for the tests.
|
||||
class ExampleInfo {
|
||||
ExampleInfo(File file, Directory examplesLibDir)
|
||||
: importPath = _getImportPath(file, examplesLibDir),
|
||||
importName = '' {
|
||||
: importPath = _getImportPath(file, examplesLibDir),
|
||||
importName = '' {
|
||||
importName = importPath.replaceAll(RegExp(r'\.dart$'), '').replaceAll(RegExp(r'\W'), '_');
|
||||
}
|
||||
|
||||
@@ -91,8 +86,10 @@ class ExampleInfo {
|
||||
String importName;
|
||||
|
||||
static String _getImportPath(File example, Directory examplesLibDir) {
|
||||
final String relativePath =
|
||||
path.relative(example.absolute.path, from: examplesLibDir.absolute.path);
|
||||
final String relativePath = path.relative(
|
||||
example.absolute.path,
|
||||
from: examplesLibDir.absolute.path,
|
||||
);
|
||||
// So that Windows paths are proper URIs in the import statements.
|
||||
return path.toUri(relativePath).toFilePath(windows: false);
|
||||
}
|
||||
@@ -108,9 +105,7 @@ Future<File> generateTest(Directory apiDir) async {
|
||||
<String>['git', 'ls-files', '**/*.dart'],
|
||||
workingDirectory: examplesLibDir,
|
||||
quiet: true,
|
||||
)).replaceAll(r'\', '/')
|
||||
.trim()
|
||||
.split('\n');
|
||||
)).replaceAll(r'\', '/').trim().split('\n');
|
||||
final Iterable<File> examples = gitFiles.map<File>((String examplePath) {
|
||||
return _kFilesystem.file(path.join(examplesLibDir.absolute.path, examplePath));
|
||||
});
|
||||
@@ -126,7 +121,7 @@ Future<File> generateTest(Directory apiDir) async {
|
||||
"import 'package:flutter_test/flutter_test.dart';",
|
||||
"import 'package:integration_test/integration_test.dart';",
|
||||
for (final ExampleInfo info in infoList)
|
||||
"import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};"
|
||||
"import 'package:flutter_api_samples/${info.importPath}' as ${info.importName};",
|
||||
]..sort();
|
||||
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
@@ -172,8 +167,9 @@ void main() {
|
||||
}
|
||||
buffer.writeln('}');
|
||||
|
||||
final File integrationTest =
|
||||
apiDir.childDirectory('integration_test').childFile('smoke_integration_test.dart');
|
||||
final File integrationTest = apiDir
|
||||
.childDirectory('integration_test')
|
||||
.childFile('smoke_integration_test.dart');
|
||||
integrationTest.createSync(recursive: true);
|
||||
integrationTest.writeAsStringSync(buffer.toString());
|
||||
return integrationTest;
|
||||
@@ -206,40 +202,40 @@ Future<String> runCommand(
|
||||
workingDirectory: workingDirectory.absolute.path,
|
||||
environment: environment,
|
||||
);
|
||||
process.stdout.listen(
|
||||
(List<int> event) {
|
||||
stdoutOutput.addAll(event);
|
||||
combinedOutput.addAll(event);
|
||||
if (!quiet) {
|
||||
stdout.add(event);
|
||||
}
|
||||
},
|
||||
onDone: () async => stdoutComplete.complete(),
|
||||
);
|
||||
process.stderr.listen(
|
||||
(List<int> event) {
|
||||
combinedOutput.addAll(event);
|
||||
if (!quiet) {
|
||||
stderr.add(event);
|
||||
}
|
||||
},
|
||||
onDone: () async => stderrComplete.complete(),
|
||||
);
|
||||
process.stdout.listen((List<int> event) {
|
||||
stdoutOutput.addAll(event);
|
||||
combinedOutput.addAll(event);
|
||||
if (!quiet) {
|
||||
stdout.add(event);
|
||||
}
|
||||
}, onDone: () async => stdoutComplete.complete());
|
||||
process.stderr.listen((List<int> event) {
|
||||
combinedOutput.addAll(event);
|
||||
if (!quiet) {
|
||||
stderr.add(event);
|
||||
}
|
||||
}, onDone: () async => stderrComplete.complete());
|
||||
} on ProcessException catch (e) {
|
||||
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
|
||||
'failed with:\n$e');
|
||||
stderr.writeln(
|
||||
'Running "${cmd.join(' ')}" in ${workingDirectory.path} '
|
||||
'failed with:\n$e',
|
||||
);
|
||||
exitCode = 2;
|
||||
return utf8.decode(stdoutOutput);
|
||||
} on ArgumentError catch (e) {
|
||||
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} '
|
||||
'failed with:\n$e');
|
||||
stderr.writeln(
|
||||
'Running "${cmd.join(' ')}" in ${workingDirectory.path} '
|
||||
'failed with:\n$e',
|
||||
);
|
||||
exitCode = 3;
|
||||
return utf8.decode(stdoutOutput);
|
||||
}
|
||||
|
||||
final int processExitCode = await allComplete();
|
||||
if (processExitCode != 0) {
|
||||
stderr.writeln('Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode');
|
||||
stderr.writeln(
|
||||
'Running "${cmd.join(' ')}" in ${workingDirectory.path} exited with code $processExitCode',
|
||||
);
|
||||
exitCode = processExitCode;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ void _validate(List<String> args) {
|
||||
}
|
||||
if (!File('../engine/src/flutter/DEPS').existsSync()) {
|
||||
errors = true;
|
||||
print('This program assumes the engine directory is a sibling to the flutter repository directory.');
|
||||
print(
|
||||
'This program assumes the engine directory is a sibling to the flutter repository directory.',
|
||||
);
|
||||
}
|
||||
if (args.length != 1) {
|
||||
errors = true;
|
||||
@@ -46,57 +48,45 @@ Future<void> main(List<String> args) async {
|
||||
}
|
||||
|
||||
Future<void> _fetchUpstream([String workingDirectory = '.']) async {
|
||||
print('Fetching remotes for "$workingDirectory" - you may be prompted for SSH credentials by git.');
|
||||
final ProcessResult fetchResult = await Process.run(
|
||||
'git',
|
||||
<String>[
|
||||
'fetch',
|
||||
'--all',
|
||||
],
|
||||
workingDirectory: workingDirectory,
|
||||
print(
|
||||
'Fetching remotes for "$workingDirectory" - you may be prompted for SSH credentials by git.',
|
||||
);
|
||||
final ProcessResult fetchResult = await Process.run('git', <String>[
|
||||
'fetch',
|
||||
'--all',
|
||||
], workingDirectory: workingDirectory);
|
||||
if (fetchResult.exitCode != 0) {
|
||||
throw Exception('Failed to fetch upstream in repository $workingDirectory');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _tagsForRevision(String flutterRevision) async {
|
||||
final ProcessResult tagResult = await Process.run(
|
||||
'git',
|
||||
<String>[
|
||||
'tag',
|
||||
'--contains',
|
||||
flutterRevision,
|
||||
],
|
||||
);
|
||||
final ProcessResult tagResult = await Process.run('git', <String>[
|
||||
'tag',
|
||||
'--contains',
|
||||
flutterRevision,
|
||||
]);
|
||||
return tagResult.stdout as String;
|
||||
}
|
||||
|
||||
Future<bool> containsRevision(String ancestorRevision, String revision) async {
|
||||
final ProcessResult result = await Process.run(
|
||||
'git',
|
||||
<String>[
|
||||
'merge-base',
|
||||
'--is-ancestor',
|
||||
ancestorRevision,
|
||||
revision,
|
||||
],
|
||||
workingDirectory: engineRepo,
|
||||
);
|
||||
final ProcessResult result = await Process.run('git', <String>[
|
||||
'merge-base',
|
||||
'--is-ancestor',
|
||||
ancestorRevision,
|
||||
revision,
|
||||
], workingDirectory: engineRepo);
|
||||
return result.exitCode == 0;
|
||||
}
|
||||
|
||||
Stream<FlutterEngineRevision> _logEngineVersions() async* {
|
||||
final ProcessResult result = await Process.run(
|
||||
'git',
|
||||
<String>[
|
||||
'log',
|
||||
'--oneline',
|
||||
'-p',
|
||||
'--',
|
||||
'bin/internal/engine.version',
|
||||
],
|
||||
);
|
||||
final ProcessResult result = await Process.run('git', <String>[
|
||||
'log',
|
||||
'--oneline',
|
||||
'-p',
|
||||
'--',
|
||||
'bin/internal/engine.version',
|
||||
]);
|
||||
if (result.exitCode != 0) {
|
||||
print(result.stderr);
|
||||
throw Exception('Failed to log bin/internal/engine.version');
|
||||
|
||||
@@ -67,12 +67,7 @@ const String dataDir = 'dev/tools/gen_defaults/data';
|
||||
Future<void> main(List<String> args) async {
|
||||
// Parse arguments
|
||||
final ArgParser parser = ArgParser();
|
||||
parser.addFlag(
|
||||
'verbose',
|
||||
abbr: 'v',
|
||||
help: 'Enable verbose output',
|
||||
negatable: false,
|
||||
);
|
||||
parser.addFlag('verbose', abbr: 'v', help: 'Enable verbose output', negatable: false);
|
||||
final ArgResults argResults = parser.parse(args);
|
||||
final bool verbose = argResults['verbose'] as bool;
|
||||
|
||||
@@ -96,6 +91,8 @@ Future<void> main(List<String> args) async {
|
||||
final Map<String, dynamic> colorLightTokens = _readTokenFile(File('$dataDir/color_light.json'));
|
||||
final Map<String, dynamic> colorDarkTokens = _readTokenFile(File('$dataDir/color_dark.json'));
|
||||
|
||||
// The verifyTokenTemplatesUpdateCorrectFiles check in dev/bots/analyze.dart depends on the exact formatting of the next few lines.
|
||||
// dart format off
|
||||
// Generate tokens files.
|
||||
ChipTemplate('Chip', '$materialLib/chip.dart', tokens).updateFile();
|
||||
ActionChipTemplate('ActionChip', '$materialLib/action_chip.dart', tokens).updateFile();
|
||||
@@ -150,6 +147,7 @@ Future<void> main(List<String> args) async {
|
||||
TextFieldTemplate('TextField', '$materialLib/text_field.dart', tokens).updateFile();
|
||||
TabsTemplate('Tabs', '$materialLib/tabs.dart', tokens).updateFile();
|
||||
TypographyTemplate('Typography', '$materialLib/typography.dart', tokens).updateFile();
|
||||
// dart format on
|
||||
|
||||
tokenLogger.printVersionUsage(verbose: verbose);
|
||||
tokenLogger.printTokensUsage(verbose: verbose);
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class ActionChipTemplate extends TokenTemplate {
|
||||
const ActionChipTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const ActionChipTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
static const String tokenGroup = 'md.comp.assist-chip';
|
||||
|
||||
@@ -6,10 +6,7 @@ import 'template.dart';
|
||||
|
||||
class AppBarTemplate extends TokenTemplate {
|
||||
const AppBarTemplate(super.blockName, super.fileName, super.tokens)
|
||||
: super(
|
||||
colorSchemePrefix: '_colors.',
|
||||
textThemePrefix: '_textTheme.',
|
||||
);
|
||||
: super(colorSchemePrefix: '_colors.', textThemePrefix: '_textTheme.');
|
||||
|
||||
@override
|
||||
String generate() => '''
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class BadgeTemplate extends TokenTemplate {
|
||||
const BadgeTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const BadgeTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class BannerTemplate extends TokenTemplate {
|
||||
const BannerTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const BannerTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class BottomAppBarTemplate extends TokenTemplate {
|
||||
const BottomAppBarTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const BottomAppBarTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import 'template.dart';
|
||||
|
||||
class ButtonTemplate extends TokenTemplate {
|
||||
const ButtonTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||
const ButtonTemplate(
|
||||
this.tokenGroup,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import 'template.dart';
|
||||
|
||||
class CardTemplate extends TokenTemplate {
|
||||
const CardTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||
const CardTemplate(
|
||||
this.tokenGroup,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class CheckboxTemplate extends TokenTemplate {
|
||||
const CheckboxTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const CheckboxTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class ChipTemplate extends TokenTemplate {
|
||||
const ChipTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const ChipTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
static const String tokenGroup = 'md.comp.filter-chip';
|
||||
|
||||
@@ -6,7 +6,13 @@ import 'template.dart';
|
||||
import 'token_logger.dart';
|
||||
|
||||
class ColorSchemeTemplate extends TokenTemplate {
|
||||
ColorSchemeTemplate(this._colorTokensLight, this._colorTokensDark, super.blockName, super.fileName, super.tokens);
|
||||
ColorSchemeTemplate(
|
||||
this._colorTokensLight,
|
||||
this._colorTokensDark,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens,
|
||||
);
|
||||
|
||||
// Map of light color scheme token data from tokens.
|
||||
final Map<String, dynamic> _colorTokensLight;
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class DatePickerTemplate extends TokenTemplate {
|
||||
const DatePickerTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const DatePickerTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
String _layerOpacity(String layerToken) {
|
||||
@@ -26,9 +29,9 @@ class DatePickerTemplate extends TokenTemplate {
|
||||
String _stateColor(String componentToken, String? type, String state) {
|
||||
final String baseColor = color(
|
||||
type != null
|
||||
? '$componentToken.$type.$state.state-layer.color'
|
||||
: '$componentToken.$state.state-layer.color',
|
||||
''
|
||||
? '$componentToken.$type.$state.state-layer.color'
|
||||
: '$componentToken.$state.state-layer.color',
|
||||
'',
|
||||
);
|
||||
if (baseColor.isEmpty) {
|
||||
return 'null';
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class DialogTemplate extends TokenTemplate {
|
||||
const DialogTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const DialogTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class ExpansionTileTemplate extends TokenTemplate {
|
||||
const ExpansionTileTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const ExpansionTileTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class FABTemplate extends TokenTemplate {
|
||||
const FABTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const FABTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class FilterChipTemplate extends TokenTemplate {
|
||||
const FilterChipTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const FilterChipTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
static const String tokenGroup = 'md.comp.filter-chip';
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import 'template.dart';
|
||||
|
||||
class IconButtonTemplate extends TokenTemplate {
|
||||
const IconButtonTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||
const IconButtonTemplate(
|
||||
this.tokenGroup,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
@@ -185,7 +189,8 @@ class IconButtonTemplate extends TokenTemplate {
|
||||
}
|
||||
|
||||
String _minimumSize() {
|
||||
if (tokenAvailable('$tokenGroup.container.height') && tokenAvailable('$tokenGroup.container.width')) {
|
||||
if (tokenAvailable('$tokenGroup.container.height') &&
|
||||
tokenAvailable('$tokenGroup.container.width')) {
|
||||
return '''
|
||||
|
||||
const MaterialStatePropertyAll<Size>(Size(${getToken('$tokenGroup.container.width')}, ${getToken('$tokenGroup.container.height')}))''';
|
||||
@@ -313,5 +318,4 @@ class _${blockName}DefaultsM3 extends ButtonStyle {
|
||||
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
|
||||
}
|
||||
''';
|
||||
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class InputChipTemplate extends TokenTemplate {
|
||||
const InputChipTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const InputChipTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
static const String tokenGroup = 'md.comp.input-chip';
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class InputDecoratorTemplate extends TokenTemplate {
|
||||
const InputDecoratorTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const InputDecoratorTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -54,7 +57,7 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme {
|
||||
}
|
||||
if (states.contains(MaterialState.error)) {
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return ${mergedBorder('md.comp.filled-text-field.error.focus.active-indicator','md.comp.filled-text-field.focus.active-indicator')};
|
||||
return ${mergedBorder('md.comp.filled-text-field.error.focus.active-indicator', 'md.comp.filled-text-field.focus.active-indicator')};
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return ${border('md.comp.filled-text-field.error.hover.active-indicator')};
|
||||
@@ -77,7 +80,7 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme {
|
||||
}
|
||||
if (states.contains(MaterialState.error)) {
|
||||
if (states.contains(MaterialState.focused)) {
|
||||
return ${mergedBorder('md.comp.outlined-text-field.error.focus.outline','md.comp.outlined-text-field.focus.outline')};
|
||||
return ${mergedBorder('md.comp.outlined-text-field.error.focus.outline', 'md.comp.outlined-text-field.focus.outline')};
|
||||
}
|
||||
if (states.contains(MaterialState.hovered)) {
|
||||
return ${border('md.comp.outlined-text-field.error.hover.outline')};
|
||||
@@ -221,15 +224,17 @@ class _${blockName}DefaultsM3 extends InputDecorationTheme {
|
||||
|
||||
/// Generate a [BorderSide] for the given components.
|
||||
String mergedBorder(String componentToken1, String componentToken2) {
|
||||
final String borderColor = componentColor(componentToken1)!= 'null'
|
||||
? componentColor(componentToken1)
|
||||
: componentColor(componentToken2);
|
||||
final double width = (
|
||||
getToken('$componentToken1.width', optional: true) ??
|
||||
getToken('$componentToken1.height', optional: true) ??
|
||||
getToken('$componentToken2.width', optional: true) ??
|
||||
getToken('$componentToken2.height', optional: true) ??
|
||||
1.0) as double;
|
||||
final String borderColor =
|
||||
componentColor(componentToken1) != 'null'
|
||||
? componentColor(componentToken1)
|
||||
: componentColor(componentToken2);
|
||||
final double width =
|
||||
(getToken('$componentToken1.width', optional: true) ??
|
||||
getToken('$componentToken1.height', optional: true) ??
|
||||
getToken('$componentToken2.width', optional: true) ??
|
||||
getToken('$componentToken2.height', optional: true) ??
|
||||
1.0)
|
||||
as double;
|
||||
return 'BorderSide(color: $borderColor${width != 1.0 ? ", width: $width" : ""})';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class ListTileTemplate extends TokenTemplate {
|
||||
const ListTileTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const ListTileTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class MenuTemplate extends TokenTemplate {
|
||||
const MenuTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const MenuTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
@@ -247,10 +250,7 @@ class _MenuDefaultsM3 extends MenuStyle {
|
||||
|
||||
@override
|
||||
MaterialStateProperty<Color?>? get surfaceTintColor {
|
||||
return ${componentColor('md.comp.menu.container.surface-tint-layer') == 'null'
|
||||
? 'const MaterialStatePropertyAll<Color?>(Colors.transparent)'
|
||||
: 'MaterialStatePropertyAll<Color?>(${componentColor('md.comp.menu.container.surface-tint-layer')})'
|
||||
};
|
||||
return ${componentColor('md.comp.menu.container.surface-tint-layer') == 'null' ? 'const MaterialStatePropertyAll<Color?>(Colors.transparent)' : 'MaterialStatePropertyAll<Color?>(${componentColor('md.comp.menu.container.surface-tint-layer')})'};
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -8,33 +8,37 @@ import 'token_logger.dart';
|
||||
class MotionTemplate extends TokenTemplate {
|
||||
/// Since we generate the tokens dynamically, we need to store them and log
|
||||
/// them manually, instead of using [getToken].
|
||||
MotionTemplate(String blockName, String fileName, this.tokens, this.tokensLogger) : super(blockName, fileName, tokens);
|
||||
MotionTemplate(String blockName, String fileName, this.tokens, this.tokensLogger)
|
||||
: super(blockName, fileName, tokens);
|
||||
Map<String, dynamic> tokens;
|
||||
TokenLogger tokensLogger;
|
||||
|
||||
// List of duration tokens.
|
||||
late List<MapEntry<String, dynamic>> durationTokens = tokens.entries.where(
|
||||
(MapEntry<String, dynamic> entry) => entry.key.contains('.duration.')
|
||||
).toList()
|
||||
..sort(
|
||||
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) => (a.value as double).compareTo(b.value as double)
|
||||
);
|
||||
late List<MapEntry<String, dynamic>> durationTokens =
|
||||
tokens.entries
|
||||
.where((MapEntry<String, dynamic> entry) => entry.key.contains('.duration.'))
|
||||
.toList()
|
||||
..sort(
|
||||
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) =>
|
||||
(a.value as double).compareTo(b.value as double),
|
||||
);
|
||||
|
||||
// List of easing curve tokens.
|
||||
late List<MapEntry<String, dynamic>> easingCurveTokens = tokens.entries.where(
|
||||
(MapEntry<String, dynamic> entry) => entry.key.contains('.easing.')
|
||||
).toList()
|
||||
..sort(
|
||||
// Sort the legacy curves at the end of the list.
|
||||
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) => a.key.contains('legacy') ? 1 : a.key.compareTo(b.key)
|
||||
);
|
||||
late List<MapEntry<String, dynamic>> easingCurveTokens =
|
||||
tokens.entries
|
||||
.where((MapEntry<String, dynamic> entry) => entry.key.contains('.easing.'))
|
||||
.toList()
|
||||
..sort(
|
||||
// Sort the legacy curves at the end of the list.
|
||||
(MapEntry<String, dynamic> a, MapEntry<String, dynamic> b) =>
|
||||
a.key.contains('legacy') ? 1 : a.key.compareTo(b.key),
|
||||
);
|
||||
|
||||
String durationTokenString(String token, dynamic tokenValue) {
|
||||
tokensLogger.log(token);
|
||||
final String tokenName = token.split('.').last.replaceAll('-', '').replaceFirst('Ms', '');
|
||||
final int milliseconds = (tokenValue as double).toInt();
|
||||
return
|
||||
'''
|
||||
return '''
|
||||
/// The $tokenName duration (${milliseconds}ms) in the Material specification.
|
||||
///
|
||||
/// See also:
|
||||
@@ -47,11 +51,12 @@ class MotionTemplate extends TokenTemplate {
|
||||
|
||||
String easingCurveTokenString(String token, dynamic tokenValue) {
|
||||
tokensLogger.log(token);
|
||||
final String tokenName = token
|
||||
.replaceFirst('md.sys.motion.easing.', '')
|
||||
.replaceAllMapped(RegExp(r'[-\.](\w)'), (Match match) {
|
||||
final String tokenName = token.replaceFirst('md.sys.motion.easing.', '').replaceAllMapped(
|
||||
RegExp(r'[-\.](\w)'),
|
||||
(Match match) {
|
||||
return match.group(1)!.toUpperCase();
|
||||
});
|
||||
},
|
||||
);
|
||||
return '''
|
||||
/// The $tokenName easing curve in the Material specification.
|
||||
///
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class NavigationBarTemplate extends TokenTemplate {
|
||||
const NavigationBarTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const NavigationBarTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class NavigationDrawerTemplate extends TokenTemplate {
|
||||
const NavigationDrawerTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const NavigationDrawerTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class NavigationRailTemplate extends TokenTemplate {
|
||||
const NavigationRailTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const NavigationRailTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class PopupMenuTemplate extends TokenTemplate {
|
||||
const PopupMenuTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const PopupMenuTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class ProgressIndicatorTemplate extends TokenTemplate {
|
||||
const ProgressIndicatorTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const ProgressIndicatorTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class RadioTemplate extends TokenTemplate {
|
||||
const RadioTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const RadioTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,13 +5,18 @@
|
||||
import 'template.dart';
|
||||
|
||||
class SearchBarTemplate extends TokenTemplate {
|
||||
const SearchBarTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const SearchBarTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
String _surfaceTint() {
|
||||
final String? color = colorOrTransparent('md.comp.search-bar.container.surface-tint-layer.color');
|
||||
final String? color = colorOrTransparent(
|
||||
'md.comp.search-bar.container.surface-tint-layer.color',
|
||||
);
|
||||
final String surfaceTintColor = 'MaterialStatePropertyAll<Color>($color);';
|
||||
if (color == 'Colors.transparent') {
|
||||
return 'const $surfaceTintColor';
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class SearchViewTemplate extends TokenTemplate {
|
||||
const SearchViewTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const SearchViewTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import 'template.dart';
|
||||
|
||||
class SegmentedButtonTemplate extends TokenTemplate {
|
||||
const SegmentedButtonTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||
const SegmentedButtonTemplate(
|
||||
this.tokenGroup,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
import 'template.dart';
|
||||
|
||||
class SliderTemplate extends TokenTemplate {
|
||||
const SliderTemplate(this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||
const SliderTemplate(
|
||||
this.tokenGroup,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
|
||||
@@ -6,8 +6,11 @@ import 'template.dart';
|
||||
|
||||
class SnackbarTemplate extends TokenTemplate {
|
||||
const SnackbarTemplate(
|
||||
this.tokenGroup, super.blockName, super.fileName, super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.'
|
||||
this.tokenGroup,
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
final String tokenGroup;
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class SwitchTemplate extends TokenTemplate {
|
||||
const SwitchTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const SwitchTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
});
|
||||
|
||||
@@ -231,5 +234,4 @@ class _SwitchConfigM3 with _SwitchConfig {
|
||||
Size get switchMinSize => const Size(kMinInteractiveDimension, kMinInteractiveDimension - 8.0);
|
||||
}
|
||||
''';
|
||||
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
import 'template.dart';
|
||||
|
||||
class TabsTemplate extends TokenTemplate {
|
||||
const TabsTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const TabsTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
@@ -160,5 +163,4 @@ class _${blockName}SecondaryDefaultsM3 extends TabBarThemeData {
|
||||
static double indicatorWeight = ${getToken('md.comp.secondary-navigation-tab.active-indicator.height')};
|
||||
}
|
||||
''';
|
||||
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ import 'token_logger.dart';
|
||||
|
||||
/// Base class for code generation templates.
|
||||
abstract class TokenTemplate {
|
||||
const TokenTemplate(this.blockName, this.fileName, this._tokens, {
|
||||
const TokenTemplate(
|
||||
this.blockName,
|
||||
this.fileName,
|
||||
this._tokens, {
|
||||
this.colorSchemePrefix = 'Theme.of(context).colorScheme.',
|
||||
this.textThemePrefix = 'Theme.of(context).textTheme.'
|
||||
this.textThemePrefix = 'Theme.of(context).textTheme.',
|
||||
});
|
||||
|
||||
/// Name of the code block that this template will generate.
|
||||
@@ -58,6 +61,13 @@ abstract class TokenTemplate {
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
''';
|
||||
|
||||
// TODO(goderbauer): Update the script to output auto-formatted code and remove
|
||||
// "dart format off/on" from headerComment and footerComment.
|
||||
static const String footerComment = '''
|
||||
// dart format on
|
||||
''';
|
||||
|
||||
static const String endGeneratedComment = '''
|
||||
@@ -95,6 +105,7 @@ abstract class TokenTemplate {
|
||||
buffer.write(beginComment);
|
||||
buffer.write(headerComment);
|
||||
buffer.write(generate());
|
||||
buffer.write(footerComment);
|
||||
buffer.write(endComment);
|
||||
buffer.write(contentAfterBlock);
|
||||
File(fileName).writeAsStringSync(buffer.toString());
|
||||
@@ -194,8 +205,14 @@ abstract class TokenTemplate {
|
||||
if (!tokenAvailable(widthToken) && !tokenAvailable(heightToken)) {
|
||||
throw Exception('Unable to find width, height, or size tokens for $componentToken');
|
||||
}
|
||||
final String? width = _numToString(tokenAvailable(widthToken) ? getToken(widthToken)! as num : double.infinity, 0);
|
||||
final String? height = _numToString(tokenAvailable(heightToken) ? getToken(heightToken)! as num : double.infinity, 0);
|
||||
final String? width = _numToString(
|
||||
tokenAvailable(widthToken) ? getToken(widthToken)! as num : double.infinity,
|
||||
0,
|
||||
);
|
||||
final String? height = _numToString(
|
||||
tokenAvailable(heightToken) ? getToken(heightToken)! as num : double.infinity,
|
||||
0,
|
||||
);
|
||||
return 'const Size($width, $height)';
|
||||
}
|
||||
return 'const Size.square(${_numToString(getToken(sizeToken))})';
|
||||
@@ -207,8 +224,8 @@ abstract class TokenTemplate {
|
||||
/// - "SHAPE_FAMILY_ROUNDED_CORNERS" which maps to [RoundedRectangleBorder].
|
||||
/// - "SHAPE_FAMILY_CIRCULAR" which maps to a [StadiumBorder].
|
||||
String shape(String componentToken, [String prefix = 'const ']) {
|
||||
|
||||
final Map<String, dynamic> shape = getToken(getToken('$componentToken.shape') as String) as Map<String, dynamic>;
|
||||
final Map<String, dynamic> shape =
|
||||
getToken(getToken('$componentToken.shape') as String) as Map<String, dynamic>;
|
||||
switch (shape['family']) {
|
||||
case 'SHAPE_FAMILY_ROUNDED_CORNERS':
|
||||
final double topLeft = shape['topLeft'] as double;
|
||||
@@ -223,18 +240,18 @@ abstract class TokenTemplate {
|
||||
}
|
||||
if (topLeft == topRight && bottomLeft == bottomRight) {
|
||||
return '${prefix}RoundedRectangleBorder(borderRadius: BorderRadius.vertical('
|
||||
'${topLeft > 0 ? 'top: Radius.circular($topLeft)':''}'
|
||||
'${topLeft > 0 && bottomLeft > 0 ? ',':''}'
|
||||
'${bottomLeft > 0 ? 'bottom: Radius.circular($bottomLeft)':''}'
|
||||
'))';
|
||||
'${topLeft > 0 ? 'top: Radius.circular($topLeft)' : ''}'
|
||||
'${topLeft > 0 && bottomLeft > 0 ? ',' : ''}'
|
||||
'${bottomLeft > 0 ? 'bottom: Radius.circular($bottomLeft)' : ''}'
|
||||
'))';
|
||||
}
|
||||
return '${prefix}RoundedRectangleBorder(borderRadius: '
|
||||
'BorderRadius.only('
|
||||
'topLeft: Radius.circular(${shape['topLeft']}), '
|
||||
'topRight: Radius.circular(${shape['topRight']}), '
|
||||
'bottomLeft: Radius.circular(${shape['bottomLeft']}), '
|
||||
'bottomRight: Radius.circular(${shape['bottomRight']})))';
|
||||
case 'SHAPE_FAMILY_CIRCULAR':
|
||||
'BorderRadius.only('
|
||||
'topLeft: Radius.circular(${shape['topLeft']}), '
|
||||
'topRight: Radius.circular(${shape['topRight']}), '
|
||||
'bottomLeft: Radius.circular(${shape['bottomLeft']}), '
|
||||
'bottomRight: Radius.circular(${shape['bottomRight']})))';
|
||||
case 'SHAPE_FAMILY_CIRCULAR':
|
||||
return '${prefix}StadiumBorder()';
|
||||
}
|
||||
print('Unsupported shape family type: ${shape['family']} for $componentToken');
|
||||
@@ -243,27 +260,24 @@ abstract class TokenTemplate {
|
||||
|
||||
/// Generate a [BorderSide] for the given component.
|
||||
String border(String componentToken) {
|
||||
|
||||
if (!tokenAvailable('$componentToken.color')) {
|
||||
return 'null';
|
||||
}
|
||||
final String borderColor = componentColor(componentToken);
|
||||
final double width = (
|
||||
getToken('$componentToken.width', optional: true) ??
|
||||
getToken('$componentToken.height', optional: true) ??
|
||||
1.0
|
||||
) as double;
|
||||
final double width =
|
||||
(getToken('$componentToken.width', optional: true) ??
|
||||
getToken('$componentToken.height', optional: true) ??
|
||||
1.0)
|
||||
as double;
|
||||
return 'BorderSide(color: $borderColor${width != 1.0 ? ", width: $width" : ""})';
|
||||
}
|
||||
|
||||
/// Generate a [TextTheme] text style name for the given component token.
|
||||
String textStyle(String componentToken) {
|
||||
|
||||
return '$textThemePrefix${getToken("$componentToken.text-style")}';
|
||||
}
|
||||
|
||||
String textStyleWithColor(String componentToken) {
|
||||
|
||||
if (!tokenAvailable('$componentToken.text-style')) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
import 'template.dart';
|
||||
|
||||
class TimePickerTemplate extends TokenTemplate {
|
||||
const TimePickerTemplate(super.blockName, super.fileName, super.tokens, {
|
||||
const TimePickerTemplate(
|
||||
super.blockName,
|
||||
super.fileName,
|
||||
super.tokens, {
|
||||
super.colorSchemePrefix = '_colors.',
|
||||
super.textThemePrefix = '_textTheme.'
|
||||
super.textThemePrefix = '_textTheme.',
|
||||
});
|
||||
|
||||
static const String tokenGroup = 'md.comp.time-picker';
|
||||
|
||||
@@ -13,8 +13,8 @@ class TokenLogger {
|
||||
|
||||
void init({
|
||||
required Map<String, dynamic> allTokens,
|
||||
required Map<String, List<String>> versionMap
|
||||
}){
|
||||
required Map<String, List<String>> versionMap,
|
||||
}) {
|
||||
_allTokens = allTokens;
|
||||
_versionMap = versionMap;
|
||||
}
|
||||
@@ -83,7 +83,11 @@ class TokenLogger {
|
||||
|
||||
if (_unavailableTokens.isNotEmpty) {
|
||||
print('');
|
||||
print('\x1B[31m' 'Some referenced tokens do not exist: ${_unavailableTokens.length}' '\x1B[0m');
|
||||
print(
|
||||
'\x1B[31m'
|
||||
'Some referenced tokens do not exist: ${_unavailableTokens.length}'
|
||||
'\x1B[0m',
|
||||
);
|
||||
for (final String token in _unavailableTokens) {
|
||||
print(' $token');
|
||||
}
|
||||
|
||||
@@ -20,21 +20,51 @@ abstract final class _M3Typography {
|
||||
|
||||
String _textTheme(String name, String baseline) {
|
||||
final StringBuffer theme = StringBuffer('static const TextTheme $name = TextTheme(\n');
|
||||
theme.writeln(' displayLarge: ${_textStyleDef('md.sys.typescale.display-large', '$name displayLarge 2021', baseline)},');
|
||||
theme.writeln(' displayMedium: ${_textStyleDef('md.sys.typescale.display-medium', '$name displayMedium 2021', baseline)},');
|
||||
theme.writeln(' displaySmall: ${_textStyleDef('md.sys.typescale.display-small', '$name displaySmall 2021', baseline)},');
|
||||
theme.writeln(' headlineLarge: ${_textStyleDef('md.sys.typescale.headline-large', '$name headlineLarge 2021', baseline)},');
|
||||
theme.writeln(' headlineMedium: ${_textStyleDef('md.sys.typescale.headline-medium', '$name headlineMedium 2021', baseline)},');
|
||||
theme.writeln(' headlineSmall: ${_textStyleDef('md.sys.typescale.headline-small', '$name headlineSmall 2021', baseline)},');
|
||||
theme.writeln(' titleLarge: ${_textStyleDef('md.sys.typescale.title-large', '$name titleLarge 2021', baseline)},');
|
||||
theme.writeln(' titleMedium: ${_textStyleDef('md.sys.typescale.title-medium', '$name titleMedium 2021', baseline)},');
|
||||
theme.writeln(' titleSmall: ${_textStyleDef('md.sys.typescale.title-small', '$name titleSmall 2021', baseline)},');
|
||||
theme.writeln(' labelLarge: ${_textStyleDef('md.sys.typescale.label-large', '$name labelLarge 2021', baseline)},');
|
||||
theme.writeln(' labelMedium: ${_textStyleDef('md.sys.typescale.label-medium', '$name labelMedium 2021', baseline)},');
|
||||
theme.writeln(' labelSmall: ${_textStyleDef('md.sys.typescale.label-small', '$name labelSmall 2021', baseline)},');
|
||||
theme.writeln(' bodyLarge: ${_textStyleDef('md.sys.typescale.body-large', '$name bodyLarge 2021', baseline)},');
|
||||
theme.writeln(' bodyMedium: ${_textStyleDef('md.sys.typescale.body-medium', '$name bodyMedium 2021', baseline)},');
|
||||
theme.writeln(' bodySmall: ${_textStyleDef('md.sys.typescale.body-small', '$name bodySmall 2021', baseline)},');
|
||||
theme.writeln(
|
||||
' displayLarge: ${_textStyleDef('md.sys.typescale.display-large', '$name displayLarge 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' displayMedium: ${_textStyleDef('md.sys.typescale.display-medium', '$name displayMedium 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' displaySmall: ${_textStyleDef('md.sys.typescale.display-small', '$name displaySmall 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' headlineLarge: ${_textStyleDef('md.sys.typescale.headline-large', '$name headlineLarge 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' headlineMedium: ${_textStyleDef('md.sys.typescale.headline-medium', '$name headlineMedium 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' headlineSmall: ${_textStyleDef('md.sys.typescale.headline-small', '$name headlineSmall 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' titleLarge: ${_textStyleDef('md.sys.typescale.title-large', '$name titleLarge 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' titleMedium: ${_textStyleDef('md.sys.typescale.title-medium', '$name titleMedium 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' titleSmall: ${_textStyleDef('md.sys.typescale.title-small', '$name titleSmall 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' labelLarge: ${_textStyleDef('md.sys.typescale.label-large', '$name labelLarge 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' labelMedium: ${_textStyleDef('md.sys.typescale.label-medium', '$name labelMedium 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' labelSmall: ${_textStyleDef('md.sys.typescale.label-small', '$name labelSmall 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' bodyLarge: ${_textStyleDef('md.sys.typescale.body-large', '$name bodyLarge 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' bodyMedium: ${_textStyleDef('md.sys.typescale.body-medium', '$name bodyMedium 2021', baseline)},',
|
||||
);
|
||||
theme.writeln(
|
||||
' bodySmall: ${_textStyleDef('md.sys.typescale.body-small', '$name bodySmall 2021', baseline)},',
|
||||
);
|
||||
theme.write(' );');
|
||||
return theme.toString();
|
||||
}
|
||||
@@ -57,7 +87,8 @@ abstract final class _M3Typography {
|
||||
}
|
||||
|
||||
String _fontWeight(String textStyleTokenName) {
|
||||
final String weightValue = getToken(getToken('$textStyleTokenName.weight') as String).toString();
|
||||
final String weightValue =
|
||||
getToken(getToken('$textStyleTokenName.weight') as String).toString();
|
||||
return 'FontWeight.w$weightValue';
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,11 @@ void main() {
|
||||
|
||||
// Have a test template append new parameterized content to the end of
|
||||
// the file.
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'Foobar', 'bar': 'Barfoo'};
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{
|
||||
'version': '0.0',
|
||||
'foo': 'Foobar',
|
||||
'bar': 'Barfoo',
|
||||
};
|
||||
TestTemplate('Test', tempFile.path, tokens).updateFile();
|
||||
|
||||
expect(tempFile.readAsStringSync(), '''
|
||||
@@ -58,12 +62,13 @@ void main() {
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'Foobar';
|
||||
static final String tokenBar = 'Barfoo';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Test
|
||||
''');
|
||||
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
@@ -87,15 +92,21 @@ static final String tokenBar = 'Barfoo';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'Foobar';
|
||||
static final String tokenBar = 'Barfoo';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Test
|
||||
''');
|
||||
|
||||
// Have a test template append new parameterized content to the end of
|
||||
// the file.
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'foo', 'bar': 'bar'};
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{
|
||||
'version': '0.0',
|
||||
'foo': 'foo',
|
||||
'bar': 'bar',
|
||||
};
|
||||
TestTemplate('Test', tempFile.path, tokens).updateFile();
|
||||
|
||||
expect(tempFile.readAsStringSync(), '''
|
||||
@@ -110,12 +121,13 @@ static final String tokenBar = 'Barfoo';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'foo';
|
||||
static final String tokenBar = 'bar';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Test
|
||||
''');
|
||||
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
@@ -135,7 +147,11 @@ static final String tokenBar = 'bar';
|
||||
|
||||
// Update file with a template for 'Block 1'
|
||||
{
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'foo', 'bar': 'bar'};
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{
|
||||
'version': '0.0',
|
||||
'foo': 'foo',
|
||||
'bar': 'bar',
|
||||
};
|
||||
TestTemplate('Block 1', tempFile.path, tokens).updateFile();
|
||||
}
|
||||
expect(tempFile.readAsStringSync(), '''
|
||||
@@ -150,8 +166,10 @@ static final String tokenBar = 'bar';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'foo';
|
||||
static final String tokenBar = 'bar';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Block 1
|
||||
''');
|
||||
@@ -159,7 +177,11 @@ static final String tokenBar = 'bar';
|
||||
// Update file with a template for 'Block 2', which should append but not
|
||||
// disturb the code in 'Block 1'.
|
||||
{
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'bar', 'bar': 'foo'};
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{
|
||||
'version': '0.0',
|
||||
'foo': 'bar',
|
||||
'bar': 'foo',
|
||||
};
|
||||
TestTemplate('Block 2', tempFile.path, tokens).updateFile();
|
||||
}
|
||||
expect(tempFile.readAsStringSync(), '''
|
||||
@@ -174,8 +196,10 @@ static final String tokenBar = 'bar';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'foo';
|
||||
static final String tokenBar = 'bar';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Block 1
|
||||
|
||||
@@ -186,8 +210,10 @@ static final String tokenBar = 'bar';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'bar';
|
||||
static final String tokenBar = 'foo';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Block 2
|
||||
''');
|
||||
@@ -195,7 +221,11 @@ static final String tokenBar = 'foo';
|
||||
// Update 'Block 1' again which should just update that block,
|
||||
// leaving 'Block 2' undisturbed.
|
||||
{
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{'version': '0.0', 'foo': 'FOO', 'bar': 'BAR'};
|
||||
final Map<String, dynamic> tokens = <String, dynamic>{
|
||||
'version': '0.0',
|
||||
'foo': 'FOO',
|
||||
'bar': 'BAR',
|
||||
};
|
||||
TestTemplate('Block 1', tempFile.path, tokens).updateFile();
|
||||
}
|
||||
expect(tempFile.readAsStringSync(), '''
|
||||
@@ -210,8 +240,10 @@ static final String tokenBar = 'foo';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'FOO';
|
||||
static final String tokenBar = 'BAR';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Block 1
|
||||
|
||||
@@ -222,12 +254,13 @@ static final String tokenBar = 'BAR';
|
||||
// Design token database by the script:
|
||||
// dev/tools/gen_defaults/bin/gen_defaults.dart.
|
||||
|
||||
// dart format off
|
||||
static final String tokenFoo = 'bar';
|
||||
static final String tokenBar = 'foo';
|
||||
// dart format on
|
||||
|
||||
// END GENERATED TOKEN PROPERTIES - Block 2
|
||||
''');
|
||||
|
||||
} finally {
|
||||
tempDir.deleteSync(recursive: true);
|
||||
}
|
||||
@@ -244,12 +277,13 @@ static final String tokenBar = 'foo';
|
||||
'bottomLeft': 3.0,
|
||||
'bottomRight': 4.0,
|
||||
},
|
||||
'shape.full': <String, dynamic>{
|
||||
'family': 'SHAPE_FAMILY_CIRCULAR',
|
||||
},
|
||||
'shape.full': <String, dynamic>{'family': 'SHAPE_FAMILY_CIRCULAR'},
|
||||
};
|
||||
final TestTemplate template = TestTemplate('Test', 'foobar.dart', tokens);
|
||||
expect(template.shape('foo'), 'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))');
|
||||
expect(
|
||||
template.shape('foo'),
|
||||
'const RoundedRectangleBorder(borderRadius: BorderRadius.only(topLeft: Radius.circular(1.0), topRight: Radius.circular(2.0), bottomLeft: Radius.circular(3.0), bottomRight: Radius.circular(4.0)))',
|
||||
);
|
||||
expect(template.shape('bar'), 'const StadiumBorder()');
|
||||
});
|
||||
|
||||
@@ -263,7 +297,7 @@ static final String tokenBar = 'foo';
|
||||
final ZoneSpecification spec = ZoneSpecification(
|
||||
print: (_, __, ___, String msg) {
|
||||
printLog.add(msg);
|
||||
}
|
||||
},
|
||||
);
|
||||
return Zone.current.fork(specification: spec).run<void>(testFn);
|
||||
};
|
||||
@@ -286,151 +320,190 @@ static final String tokenBar = 'foo';
|
||||
'v2.0.0': <String>['file_2.json, file_3.json'],
|
||||
};
|
||||
|
||||
test('can print empty usage', overridePrint(() {
|
||||
logger.printVersionUsage(verbose: true);
|
||||
expect(printLog, contains('Versions used: '));
|
||||
test(
|
||||
'can print empty usage',
|
||||
overridePrint(() {
|
||||
logger.printVersionUsage(verbose: true);
|
||||
expect(printLog, contains('Versions used: '));
|
||||
|
||||
logger.printTokensUsage(verbose: true);
|
||||
expect(printLog, contains('Tokens used: 0/0'));
|
||||
}));
|
||||
logger.printTokensUsage(verbose: true);
|
||||
expect(printLog, contains('Tokens used: 0/0'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('can print version usage', overridePrint(() {
|
||||
versionMap.addAll(testVersions);
|
||||
test(
|
||||
'can print version usage',
|
||||
overridePrint(() {
|
||||
versionMap.addAll(testVersions);
|
||||
|
||||
logger.printVersionUsage(verbose: false);
|
||||
logger.printVersionUsage(verbose: false);
|
||||
|
||||
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
|
||||
}));
|
||||
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('can print version usage (verbose)', overridePrint(() {
|
||||
versionMap.addAll(testVersions);
|
||||
test(
|
||||
'can print version usage (verbose)',
|
||||
overridePrint(() {
|
||||
versionMap.addAll(testVersions);
|
||||
|
||||
logger.printVersionUsage(verbose: true);
|
||||
logger.printVersionUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
|
||||
expect(printLog, contains(' v1.0.0:'));
|
||||
expect(printLog, contains(' file_1.json'));
|
||||
expect(printLog, contains(' v2.0.0:'));
|
||||
expect(printLog, contains(' file_2.json, file_3.json'));
|
||||
}));
|
||||
expect(printLog, contains('Versions used: v1.0.0, v2.0.0'));
|
||||
expect(printLog, contains(' v1.0.0:'));
|
||||
expect(printLog, contains(' file_1.json'));
|
||||
expect(printLog, contains(' v2.0.0:'));
|
||||
expect(printLog, contains(' file_2.json, file_3.json'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('can log and print tokens usage', overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
test(
|
||||
'can log and print tokens usage',
|
||||
overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
|
||||
logger.log('foo');
|
||||
logger.printTokensUsage(verbose: false);
|
||||
logger.log('foo');
|
||||
logger.printTokensUsage(verbose: false);
|
||||
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('can log and print tokens usage (verbose)', overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
test(
|
||||
'can log and print tokens usage (verbose)',
|
||||
overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
|
||||
logger.log('foo');
|
||||
logger.printTokensUsage(verbose: true);
|
||||
logger.log('foo');
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('✅ foo'));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}));
|
||||
expect(printLog, contains('✅ foo'));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('detects invalid logs', overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
test(
|
||||
'detects invalid logs',
|
||||
overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
|
||||
logger.log('baz');
|
||||
logger.log('foobar');
|
||||
logger.printTokensUsage(verbose: true);
|
||||
logger.log('baz');
|
||||
logger.log('foobar');
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('❌ foo'));
|
||||
expect(printLog, contains('Tokens used: 0/1'));
|
||||
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 2')));
|
||||
expect(printLog, contains(' baz'));
|
||||
expect(printLog, contains(' foobar'));
|
||||
}));
|
||||
expect(printLog, contains('❌ foo'));
|
||||
expect(printLog, contains('Tokens used: 0/1'));
|
||||
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 2')));
|
||||
expect(printLog, contains(' baz'));
|
||||
expect(printLog, contains(' foobar'));
|
||||
}),
|
||||
);
|
||||
|
||||
test("color function doesn't log when providing a default", overridePrint(() {
|
||||
allTokens['color_foo_req'] = 'value';
|
||||
test(
|
||||
"color function doesn't log when providing a default",
|
||||
overridePrint(() {
|
||||
allTokens['color_foo_req'] = 'value';
|
||||
|
||||
// color_foo_opt is not available, but because it has a default value, it won't warn about it
|
||||
// color_foo_opt is not available, but because it has a default value, it won't warn about it
|
||||
|
||||
TestColorTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
TestColorTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('✅ color_foo_req'));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}));
|
||||
expect(printLog, contains('✅ color_foo_req'));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('color function logs when not providing a default', overridePrint(() {
|
||||
// Nor color_foo_req or color_foo_opt are available, but only color_foo_req will be logged.
|
||||
// This mimics a token being removed, but expected to exist.
|
||||
test(
|
||||
'color function logs when not providing a default',
|
||||
overridePrint(() {
|
||||
// Nor color_foo_req or color_foo_opt are available, but only color_foo_req will be logged.
|
||||
// This mimics a token being removed, but expected to exist.
|
||||
|
||||
TestColorTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
TestColorTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('Tokens used: 0/0'));
|
||||
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 1')));
|
||||
expect(printLog, contains(' color_foo_req'));
|
||||
}));
|
||||
expect(printLog, contains('Tokens used: 0/0'));
|
||||
expect(printLog, contains(errorColoredString('Some referenced tokens do not exist: 1')));
|
||||
expect(printLog, contains(' color_foo_req'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('border function logs width token when available', overridePrint(() {
|
||||
allTokens['border_foo.color'] = 'red';
|
||||
allTokens['border_foo.width'] = 3.0;
|
||||
test(
|
||||
'border function logs width token when available',
|
||||
overridePrint(() {
|
||||
allTokens['border_foo.color'] = 'red';
|
||||
allTokens['border_foo.width'] = 3.0;
|
||||
|
||||
TestBorderTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
TestBorderTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('✅ border_foo.color'));
|
||||
expect(printLog, contains('✅ border_foo.width'));
|
||||
expect(printLog, contains('Tokens used: 2/2'));
|
||||
}));
|
||||
expect(printLog, contains('✅ border_foo.color'));
|
||||
expect(printLog, contains('✅ border_foo.width'));
|
||||
expect(printLog, contains('Tokens used: 2/2'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('border function logs height token when width token not available', overridePrint(() {
|
||||
allTokens['border_foo.color'] = 'red';
|
||||
allTokens['border_foo.height'] = 3.0;
|
||||
test(
|
||||
'border function logs height token when width token not available',
|
||||
overridePrint(() {
|
||||
allTokens['border_foo.color'] = 'red';
|
||||
allTokens['border_foo.height'] = 3.0;
|
||||
|
||||
TestBorderTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
TestBorderTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('✅ border_foo.color'));
|
||||
expect(printLog, contains('✅ border_foo.height'));
|
||||
expect(printLog, contains('Tokens used: 2/2'));
|
||||
}));
|
||||
expect(printLog, contains('✅ border_foo.color'));
|
||||
expect(printLog, contains('✅ border_foo.height'));
|
||||
expect(printLog, contains('Tokens used: 2/2'));
|
||||
}),
|
||||
);
|
||||
|
||||
test("border function doesn't log when width or height tokens not available", overridePrint(() {
|
||||
allTokens['border_foo.color'] = 'red';
|
||||
test(
|
||||
"border function doesn't log when width or height tokens not available",
|
||||
overridePrint(() {
|
||||
allTokens['border_foo.color'] = 'red';
|
||||
|
||||
TestBorderTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
TestBorderTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('✅ border_foo.color'));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}));
|
||||
expect(printLog, contains('✅ border_foo.color'));
|
||||
expect(printLog, contains('Tokens used: 1/1'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('can log and dump versions & tokens to a file', overridePrint(() {
|
||||
versionMap.addAll(testVersions);
|
||||
allTokens['foo'] = 'value';
|
||||
allTokens['bar'] = 'value';
|
||||
test(
|
||||
'can log and dump versions & tokens to a file',
|
||||
overridePrint(() {
|
||||
versionMap.addAll(testVersions);
|
||||
allTokens['foo'] = 'value';
|
||||
allTokens['bar'] = 'value';
|
||||
|
||||
logger.log('foo');
|
||||
logger.log('bar');
|
||||
logger.dumpToFile('test.json');
|
||||
logger.log('foo');
|
||||
logger.log('bar');
|
||||
logger.dumpToFile('test.json');
|
||||
|
||||
final String fileContent = File('test.json').readAsStringSync();
|
||||
expect(fileContent, contains('Versions used, v1.0.0, v2.0.0'));
|
||||
expect(fileContent, contains('bar,'));
|
||||
expect(fileContent, contains('foo'));
|
||||
}));
|
||||
final String fileContent = File('test.json').readAsStringSync();
|
||||
expect(fileContent, contains('Versions used, v1.0.0, v2.0.0'));
|
||||
expect(fileContent, contains('bar,'));
|
||||
expect(fileContent, contains('foo'));
|
||||
}),
|
||||
);
|
||||
|
||||
test('integration test', overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
allTokens['bar'] = 'value';
|
||||
test(
|
||||
'integration test',
|
||||
overridePrint(() {
|
||||
allTokens['foo'] = 'value';
|
||||
allTokens['bar'] = 'value';
|
||||
|
||||
TestTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
TestTemplate('block', 'filename', allTokens).generate();
|
||||
logger.printTokensUsage(verbose: true);
|
||||
|
||||
expect(printLog, contains('✅ foo'));
|
||||
expect(printLog, contains('✅ bar'));
|
||||
expect(printLog, contains('Tokens used: 2/2'));
|
||||
}));
|
||||
expect(printLog, contains('✅ foo'));
|
||||
expect(printLog, contains('✅ bar'));
|
||||
expect(printLog, contains('Tokens used: 2/2'));
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,25 +26,33 @@ import 'package:path/path.dart' as path;
|
||||
/// Get contents of the file that contains the physical key mapping in Chromium
|
||||
/// source.
|
||||
Future<String> getChromiumCodes() async {
|
||||
final Uri keyCodesUri = Uri.parse('https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_code_data.inc?format=TEXT');
|
||||
final Uri keyCodesUri = Uri.parse(
|
||||
'https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_code_data.inc?format=TEXT',
|
||||
);
|
||||
return utf8.decode(base64.decode(await http.read(keyCodesUri)));
|
||||
}
|
||||
|
||||
/// Get contents of the file that contains the logical key mapping in Chromium
|
||||
/// source.
|
||||
Future<String> getChromiumKeys() async {
|
||||
final Uri keyCodesUri = Uri.parse('https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_key_data.inc?format=TEXT');
|
||||
final Uri keyCodesUri = Uri.parse(
|
||||
'https://chromium.googlesource.com/codesearch/chromium/src/+/refs/heads/main/ui/events/keycodes/dom/dom_key_data.inc?format=TEXT',
|
||||
);
|
||||
return utf8.decode(base64.decode(await http.read(keyCodesUri)));
|
||||
}
|
||||
|
||||
/// Get contents of the file that contains the key codes in Android source.
|
||||
Future<String> getAndroidKeyCodes() async {
|
||||
final Uri keyCodesUri = Uri.parse('https://android.googlesource.com/platform/frameworks/native/+/main/include/android/keycodes.h?format=TEXT');
|
||||
final Uri keyCodesUri = Uri.parse(
|
||||
'https://android.googlesource.com/platform/frameworks/native/+/main/include/android/keycodes.h?format=TEXT',
|
||||
);
|
||||
return utf8.decode(base64.decode(await http.read(keyCodesUri)));
|
||||
}
|
||||
|
||||
Future<String> getWindowsKeyCodes() async {
|
||||
final Uri keyCodesUri = Uri.parse('https://raw.githubusercontent.com/tpn/winsdk-10/master/Include/10.0.10240.0/um/WinUser.h');
|
||||
final Uri keyCodesUri = Uri.parse(
|
||||
'https://raw.githubusercontent.com/tpn/winsdk-10/master/Include/10.0.10240.0/um/WinUser.h',
|
||||
);
|
||||
return http.read(keyCodesUri);
|
||||
}
|
||||
|
||||
@@ -55,17 +63,23 @@ Future<String> getWindowsKeyCodes() async {
|
||||
/// common keyboards. Other than some special keyboards and game pads, this
|
||||
/// should be OK.
|
||||
Future<String> getAndroidScanCodes() async {
|
||||
final Uri scanCodesUri = Uri.parse('https://android.googlesource.com/platform/frameworks/base/+/main/data/keyboards/Generic.kl?format=TEXT');
|
||||
final Uri scanCodesUri = Uri.parse(
|
||||
'https://android.googlesource.com/platform/frameworks/base/+/main/data/keyboards/Generic.kl?format=TEXT',
|
||||
);
|
||||
return utf8.decode(base64.decode(await http.read(scanCodesUri)));
|
||||
}
|
||||
|
||||
Future<String> getGlfwKeyCodes() async {
|
||||
final Uri keyCodesUri = Uri.parse('https://raw.githubusercontent.com/glfw/glfw/master/include/GLFW/glfw3.h');
|
||||
final Uri keyCodesUri = Uri.parse(
|
||||
'https://raw.githubusercontent.com/glfw/glfw/master/include/GLFW/glfw3.h',
|
||||
);
|
||||
return http.read(keyCodesUri);
|
||||
}
|
||||
|
||||
Future<String> getGtkKeyCodes() async {
|
||||
final Uri keyCodesUri = Uri.parse('https://gitlab.gnome.org/GNOME/gtk/-/raw/gtk-3-24/gdk/gdkkeysyms.h');
|
||||
final Uri keyCodesUri = Uri.parse(
|
||||
'https://gitlab.gnome.org/GNOME/gtk/-/raw/gtk-3-24/gdk/gdkkeysyms.h',
|
||||
);
|
||||
return http.read(keyCodesUri);
|
||||
}
|
||||
|
||||
@@ -100,7 +114,8 @@ Future<void> main(List<String> rawArguments) async {
|
||||
argParser.addOption(
|
||||
'engine-root',
|
||||
defaultsTo: path.join(flutterRoot.path, '..', 'engine', 'src', 'flutter'),
|
||||
help: 'The path to the root of the flutter/engine repository. This is used '
|
||||
help:
|
||||
'The path to the root of the flutter/engine repository. This is used '
|
||||
'to place the generated engine mapping files. If --engine-root is not '
|
||||
r'specified, it will default to $flutterRoot/../engine/src/flutter, '
|
||||
'assuming the engine gclient folder is placed at the same folder as '
|
||||
@@ -109,7 +124,8 @@ Future<void> main(List<String> rawArguments) async {
|
||||
argParser.addOption(
|
||||
'physical-data',
|
||||
defaultsTo: path.join(dataRoot, 'physical_key_data.g.json'),
|
||||
help: 'The path to where the physical key data file should be written when '
|
||||
help:
|
||||
'The path to where the physical key data file should be written when '
|
||||
'collected, and read from when generating output code. If --physical-data is '
|
||||
'not specified, the output will be written to/read from the current '
|
||||
"directory. If the output directory doesn't exist, it, and the path to "
|
||||
@@ -118,7 +134,8 @@ Future<void> main(List<String> rawArguments) async {
|
||||
argParser.addOption(
|
||||
'logical-data',
|
||||
defaultsTo: path.join(dataRoot, 'logical_key_data.g.json'),
|
||||
help: 'The path to where the logical key data file should be written when '
|
||||
help:
|
||||
'The path to where the logical key data file should be written when '
|
||||
'collected, and read from when generating output code. If --logical-data is '
|
||||
'not specified, the output will be written to/read from the current '
|
||||
"directory. If the output directory doesn't exist, it, and the path to "
|
||||
@@ -126,33 +143,48 @@ Future<void> main(List<String> rawArguments) async {
|
||||
);
|
||||
argParser.addOption(
|
||||
'code',
|
||||
defaultsTo: path.join(flutterRoot.path, 'packages', 'flutter', 'lib', 'src', 'services', 'keyboard_key.g.dart'),
|
||||
help: 'The path to where the output "keyboard_key.g.dart" file should be '
|
||||
defaultsTo: path.join(
|
||||
flutterRoot.path,
|
||||
'packages',
|
||||
'flutter',
|
||||
'lib',
|
||||
'src',
|
||||
'services',
|
||||
'keyboard_key.g.dart',
|
||||
),
|
||||
help:
|
||||
'The path to where the output "keyboard_key.g.dart" file should be '
|
||||
'written. If --code is not specified, the output will be written to the '
|
||||
'correct directory in the flutter tree. If the output directory does not '
|
||||
'exist, it, and the path to it, will be created.',
|
||||
);
|
||||
argParser.addOption(
|
||||
'maps',
|
||||
defaultsTo: path.join(flutterRoot.path, 'packages', 'flutter', 'lib', 'src', 'services', 'keyboard_maps.g.dart'),
|
||||
help: 'The path to where the output "keyboard_maps.g.dart" file should be '
|
||||
'written. If --maps is not specified, the output will be written to the '
|
||||
'correct directory in the flutter tree. If the output directory does not '
|
||||
'exist, it, and the path to it, will be created.',
|
||||
defaultsTo: path.join(
|
||||
flutterRoot.path,
|
||||
'packages',
|
||||
'flutter',
|
||||
'lib',
|
||||
'src',
|
||||
'services',
|
||||
'keyboard_maps.g.dart',
|
||||
),
|
||||
help:
|
||||
'The path to where the output "keyboard_maps.g.dart" file should be '
|
||||
'written. If --maps is not specified, the output will be written to the '
|
||||
'correct directory in the flutter tree. If the output directory does not '
|
||||
'exist, it, and the path to it, will be created.',
|
||||
);
|
||||
argParser.addFlag(
|
||||
'collect',
|
||||
negatable: false,
|
||||
help: 'If this flag is set, then collect and parse header files from '
|
||||
help:
|
||||
'If this flag is set, then collect and parse header files from '
|
||||
'Chromium and Android instead of reading pre-parsed data from '
|
||||
'"physical_key_data.g.json" and "logical_key_data.g.json", and then '
|
||||
'update these files with the fresh data.',
|
||||
);
|
||||
argParser.addFlag(
|
||||
'help',
|
||||
negatable: false,
|
||||
help: 'Print help for this command.',
|
||||
);
|
||||
argParser.addFlag('help', negatable: false, help: 'Print help for this command.');
|
||||
|
||||
final ArgResults parsedArguments = argParser.parse(rawArguments);
|
||||
|
||||
@@ -212,45 +244,59 @@ Future<void> main(List<String> rawArguments) async {
|
||||
final String logicalJson = encoder.convert(logicalData.toJson());
|
||||
File(parsedArguments['logical-data'] as String).writeAsStringSync('$logicalJson\n');
|
||||
} else {
|
||||
physicalData = PhysicalKeyData.fromJson(json.decode(await File(parsedArguments['physical-data'] as String).readAsString()) as Map<String, dynamic>);
|
||||
logicalData = LogicalKeyData.fromJson(json.decode(await File(parsedArguments['logical-data'] as String).readAsString()) as Map<String, dynamic>);
|
||||
physicalData = PhysicalKeyData.fromJson(
|
||||
json.decode(await File(parsedArguments['physical-data'] as String).readAsString())
|
||||
as Map<String, dynamic>,
|
||||
);
|
||||
logicalData = LogicalKeyData.fromJson(
|
||||
json.decode(await File(parsedArguments['logical-data'] as String).readAsString())
|
||||
as Map<String, dynamic>,
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, bool> layoutGoals = parseMapOfBool(readDataFile('layout_goals.json'));
|
||||
|
||||
await generate('key codes',
|
||||
parsedArguments['code'] as String,
|
||||
KeyboardKeysCodeGenerator(physicalData, logicalData));
|
||||
await generate('key maps',
|
||||
parsedArguments['maps'] as String,
|
||||
KeyboardMapsCodeGenerator(physicalData, logicalData));
|
||||
await generate('engine utils',
|
||||
path.join(PlatformCodeGenerator.engineRoot,
|
||||
'shell', 'platform', 'embedder', 'test_utils', 'key_codes.g.h'),
|
||||
KeyCodesCcGenerator(physicalData, logicalData));
|
||||
await generate('android utils',
|
||||
path.join(PlatformCodeGenerator.engineRoot, 'shell', 'platform',
|
||||
path.join('android', 'test', 'io', 'flutter', 'util', 'KeyCodes.java')),
|
||||
KeyCodesJavaGenerator(physicalData, logicalData));
|
||||
await generate(
|
||||
'key codes',
|
||||
parsedArguments['code'] as String,
|
||||
KeyboardKeysCodeGenerator(physicalData, logicalData),
|
||||
);
|
||||
await generate(
|
||||
'key maps',
|
||||
parsedArguments['maps'] as String,
|
||||
KeyboardMapsCodeGenerator(physicalData, logicalData),
|
||||
);
|
||||
await generate(
|
||||
'engine utils',
|
||||
path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'shell',
|
||||
'platform',
|
||||
'embedder',
|
||||
'test_utils',
|
||||
'key_codes.g.h',
|
||||
),
|
||||
KeyCodesCcGenerator(physicalData, logicalData),
|
||||
);
|
||||
await generate(
|
||||
'android utils',
|
||||
path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'shell',
|
||||
'platform',
|
||||
path.join('android', 'test', 'io', 'flutter', 'util', 'KeyCodes.java'),
|
||||
),
|
||||
KeyCodesJavaGenerator(physicalData, logicalData),
|
||||
);
|
||||
|
||||
final Map<String, PlatformCodeGenerator> platforms = <String, PlatformCodeGenerator>{
|
||||
'android': AndroidCodeGenerator(
|
||||
physicalData,
|
||||
logicalData,
|
||||
),
|
||||
'macos': MacOSCodeGenerator(
|
||||
physicalData,
|
||||
logicalData,
|
||||
layoutGoals,
|
||||
),
|
||||
'ios': IOSCodeGenerator(
|
||||
physicalData,
|
||||
logicalData,
|
||||
),
|
||||
'android': AndroidCodeGenerator(physicalData, logicalData),
|
||||
'macos': MacOSCodeGenerator(physicalData, logicalData, layoutGoals),
|
||||
'ios': IOSCodeGenerator(physicalData, logicalData),
|
||||
'windows': WindowsCodeGenerator(
|
||||
physicalData,
|
||||
logicalData,
|
||||
readDataFile('windows_scancode_logical_map.json')
|
||||
readDataFile('windows_scancode_logical_map.json'),
|
||||
),
|
||||
'linux': GtkCodeGenerator(
|
||||
physicalData,
|
||||
@@ -265,11 +311,11 @@ Future<void> main(List<String> rawArguments) async {
|
||||
readDataFile('web_logical_location_mapping.json'),
|
||||
),
|
||||
};
|
||||
await Future.wait(platforms.entries.map((MapEntry<String, PlatformCodeGenerator> entry) {
|
||||
final String platform = entry.key;
|
||||
final PlatformCodeGenerator codeGenerator = entry.value;
|
||||
return generate('$platform map',
|
||||
codeGenerator.outputPath(platform),
|
||||
codeGenerator);
|
||||
}));
|
||||
await Future.wait(
|
||||
platforms.entries.map((MapEntry<String, PlatformCodeGenerator> entry) {
|
||||
final String platform = entry.key;
|
||||
final PlatformCodeGenerator codeGenerator = entry.value;
|
||||
return generate('$platform map', codeGenerator.outputPath(platform), codeGenerator);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'logical_key_data.dart';
|
||||
import 'physical_key_data.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
|
||||
/// Generates the key mapping for Android, based on the information in the key
|
||||
/// data structure given to it.
|
||||
class AndroidCodeGenerator extends PlatformCodeGenerator {
|
||||
@@ -21,7 +20,9 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
|
||||
final StringBuffer androidKeyCodeMap = StringBuffer();
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
for (final int code in entry.androidValues) {
|
||||
androidKeyCodeMap.writeln(' put(${toHex(code, digits: 10)}L, ${toHex(entry.value, digits: 10)}L); // ${entry.constantName}');
|
||||
androidKeyCodeMap.writeln(
|
||||
' put(${toHex(code, digits: 10)}L, ${toHex(entry.value, digits: 10)}L); // ${entry.constantName}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return androidKeyCodeMap.toString().trimRight();
|
||||
@@ -32,7 +33,9 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
|
||||
final StringBuffer androidScanCodeMap = StringBuffer();
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
for (final int code in entry.androidScanCodes.cast<int>()) {
|
||||
androidScanCodeMap.writeln(' put(${toHex(code, digits: 10)}L, ${toHex(entry.usbHidCode, digits: 10)}L); // ${entry.constantName}');
|
||||
androidScanCodeMap.writeln(
|
||||
' put(${toHex(code, digits: 10)}L, ${toHex(entry.usbHidCode, digits: 10)}L); // ${entry.constantName}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return androidScanCodeMap.toString().trimRight();
|
||||
@@ -47,35 +50,38 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
|
||||
};
|
||||
goalsSource.forEach((String flagName, List<String> keys) {
|
||||
int? lineId;
|
||||
final List<String> keysString = keys.map((String keyName) {
|
||||
final PhysicalKeyEntry physicalKey = keyData.entryByName(keyName);
|
||||
final LogicalKeyEntry logicalKey = logicalData.entryByName(keyName);
|
||||
lineId ??= physicalKey.usbHidCode;
|
||||
return ' new KeyPair(${toHex(physicalKey.usbHidCode)}L, '
|
||||
'${toHex(logicalKey.value, digits: 10)}L), // ${physicalKey.name}';
|
||||
}).toList();
|
||||
lines.add(lineId!,
|
||||
' new PressingGoal(\n'
|
||||
' KeyEvent.META_${flagName}_ON,\n'
|
||||
' new KeyPair[] {\n'
|
||||
'${keysString.join('\n')}\n'
|
||||
' }),');
|
||||
final List<String> keysString =
|
||||
keys.map((String keyName) {
|
||||
final PhysicalKeyEntry physicalKey = keyData.entryByName(keyName);
|
||||
final LogicalKeyEntry logicalKey = logicalData.entryByName(keyName);
|
||||
lineId ??= physicalKey.usbHidCode;
|
||||
return ' new KeyPair(${toHex(physicalKey.usbHidCode)}L, '
|
||||
'${toHex(logicalKey.value, digits: 10)}L), // ${physicalKey.name}';
|
||||
}).toList();
|
||||
lines.add(
|
||||
lineId!,
|
||||
' new PressingGoal(\n'
|
||||
' KeyEvent.META_${flagName}_ON,\n'
|
||||
' new KeyPair[] {\n'
|
||||
'${keysString.join('\n')}\n'
|
||||
' }),',
|
||||
);
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
String get _togglingGoals {
|
||||
final OutputLines<int> lines = OutputLines<int>('Android toggling goals');
|
||||
const Map<String, String> goalsSource = <String, String>{
|
||||
'CAPS_LOCK': 'CapsLock',
|
||||
};
|
||||
const Map<String, String> goalsSource = <String, String>{'CAPS_LOCK': 'CapsLock'};
|
||||
goalsSource.forEach((String flagName, String keyName) {
|
||||
final PhysicalKeyEntry physicalKey = keyData.entryByName(keyName);
|
||||
final LogicalKeyEntry logicalKey = logicalData.entryByName(keyName);
|
||||
lines.add(physicalKey.usbHidCode,
|
||||
' new TogglingGoal(KeyEvent.META_${flagName}_ON, '
|
||||
'${toHex(physicalKey.usbHidCode)}L, '
|
||||
'${toHex(logicalKey.value, digits: 10)}L),');
|
||||
lines.add(
|
||||
physicalKey.usbHidCode,
|
||||
' new TogglingGoal(KeyEvent.META_${flagName}_ON, '
|
||||
'${toHex(physicalKey.usbHidCode)}L, '
|
||||
'${toHex(logicalKey.value, digits: 10)}L),',
|
||||
);
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
@@ -89,7 +95,9 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
|
||||
kAndroidPlane,
|
||||
];
|
||||
for (final MaskConstant constant in maskConstants) {
|
||||
buffer.writeln(' public static final long k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)}L;');
|
||||
buffer.writeln(
|
||||
' public static final long k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)}L;',
|
||||
);
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
@@ -98,8 +106,12 @@ class AndroidCodeGenerator extends PlatformCodeGenerator {
|
||||
String get templatePath => path.join(dataRoot, 'android_keyboard_map_java.tmpl');
|
||||
|
||||
@override
|
||||
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot, 'shell', 'platform',
|
||||
path.join('android', 'io', 'flutter', 'embedding', 'android', 'KeyboardMap.java'));
|
||||
String outputPath(String platform) => path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'shell',
|
||||
'platform',
|
||||
path.join('android', 'io', 'flutter', 'embedding', 'android', 'KeyboardMap.java'),
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, String> mappings() {
|
||||
|
||||
@@ -11,7 +11,7 @@ class MaskConstant {
|
||||
: this(
|
||||
name: '$platform Plane',
|
||||
value: value,
|
||||
description: 'The plane value for the private keys defined by the $platform embedding.'
|
||||
description: 'The plane value for the private keys defined by the $platform embedding.',
|
||||
);
|
||||
|
||||
final String name;
|
||||
@@ -20,9 +20,9 @@ class MaskConstant {
|
||||
|
||||
String get upperCamelName {
|
||||
return name
|
||||
.split(' ')
|
||||
.map<String>((String word) => lowerCamelToUpperCamel(word.toLowerCase()))
|
||||
.join();
|
||||
.split(' ')
|
||||
.map<String>((String word) => lowerCamelToUpperCamel(word.toLowerCase()))
|
||||
.join();
|
||||
}
|
||||
|
||||
String get lowerCamelName {
|
||||
@@ -52,7 +52,8 @@ const MaskConstant kUnicodePlane = MaskConstant(
|
||||
const MaskConstant kUnprintablePlane = MaskConstant(
|
||||
name: 'Unprintable Plane',
|
||||
value: 0x0100000000,
|
||||
description: 'The plane value for keys defined by Chromium and does not have a Unicode representation.',
|
||||
description:
|
||||
'The plane value for keys defined by Chromium and does not have a Unicode representation.',
|
||||
);
|
||||
|
||||
const MaskConstant kFlutterPlane = MaskConstant(
|
||||
@@ -64,45 +65,22 @@ const MaskConstant kFlutterPlane = MaskConstant(
|
||||
const MaskConstant kStartOfPlatformPlanes = MaskConstant(
|
||||
name: 'Start Of Platform Planes',
|
||||
value: 0x1100000000,
|
||||
description: 'The platform plane with the lowest mask value, beyond which the keys are considered autogenerated.',
|
||||
description:
|
||||
'The platform plane with the lowest mask value, beyond which the keys are considered autogenerated.',
|
||||
);
|
||||
|
||||
const MaskConstant kAndroidPlane = MaskConstant.platform(
|
||||
platform: 'Android',
|
||||
value: 0x1100000000,
|
||||
);
|
||||
const MaskConstant kAndroidPlane = MaskConstant.platform(platform: 'Android', value: 0x1100000000);
|
||||
|
||||
const MaskConstant kFuchsiaPlane = MaskConstant.platform(
|
||||
platform: 'Fuchsia',
|
||||
value: 0x1200000000,
|
||||
);
|
||||
const MaskConstant kFuchsiaPlane = MaskConstant.platform(platform: 'Fuchsia', value: 0x1200000000);
|
||||
|
||||
const MaskConstant kIosPlane = MaskConstant.platform(
|
||||
platform: 'iOS',
|
||||
value: 0x1300000000,
|
||||
);
|
||||
const MaskConstant kIosPlane = MaskConstant.platform(platform: 'iOS', value: 0x1300000000);
|
||||
|
||||
const MaskConstant kMacosPlane = MaskConstant.platform(
|
||||
platform: 'macOS',
|
||||
value: 0x1400000000,
|
||||
);
|
||||
const MaskConstant kMacosPlane = MaskConstant.platform(platform: 'macOS', value: 0x1400000000);
|
||||
|
||||
const MaskConstant kGtkPlane = MaskConstant.platform(
|
||||
platform: 'Gtk',
|
||||
value: 0x1500000000,
|
||||
);
|
||||
const MaskConstant kGtkPlane = MaskConstant.platform(platform: 'Gtk', value: 0x1500000000);
|
||||
|
||||
const MaskConstant kWindowsPlane = MaskConstant.platform(
|
||||
platform: 'Windows',
|
||||
value: 0x1600000000,
|
||||
);
|
||||
const MaskConstant kWindowsPlane = MaskConstant.platform(platform: 'Windows', value: 0x1600000000);
|
||||
|
||||
const MaskConstant kWebPlane = MaskConstant.platform(
|
||||
platform: 'Web',
|
||||
value: 0x1700000000,
|
||||
);
|
||||
const MaskConstant kWebPlane = MaskConstant.platform(platform: 'Web', value: 0x1700000000);
|
||||
|
||||
const MaskConstant kGlfwPlane = MaskConstant.platform(
|
||||
platform: 'GLFW',
|
||||
value: 0x1800000000,
|
||||
);
|
||||
const MaskConstant kGlfwPlane = MaskConstant.platform(platform: 'GLFW', value: 0x1800000000);
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'logical_key_data.dart';
|
||||
import 'physical_key_data.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
|
||||
/// Generates the key mapping for GTK, based on the information in the key
|
||||
/// data structure given to it.
|
||||
class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
@@ -28,8 +27,10 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('GTK scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.xKbScanCode != null) {
|
||||
lines.add(entry.xKbScanCode!,
|
||||
' {${toHex(entry.xKbScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}');
|
||||
lines.add(
|
||||
entry.xKbScanCode!,
|
||||
' {${toHex(entry.xKbScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -40,18 +41,17 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('GTK keyval map');
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
zipStrict(entry.gtkValues, entry.gtkNames, (int value, String name) {
|
||||
lines.add(value,
|
||||
' {${toHex(value)}, ${toHex(entry.value, digits: 11)}}, // $name');
|
||||
lines.add(value, ' {${toHex(value)}, ${toHex(entry.value, digits: 11)}}, // $name');
|
||||
});
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
static String constructMapFromModToKeys(
|
||||
Map<String, List<String>> source,
|
||||
PhysicalKeyData physicalData,
|
||||
LogicalKeyData logicalData,
|
||||
String debugFunctionName,
|
||||
Map<String, List<String>> source,
|
||||
PhysicalKeyData physicalData,
|
||||
LogicalKeyData logicalData,
|
||||
String debugFunctionName,
|
||||
) {
|
||||
final StringBuffer result = StringBuffer();
|
||||
source.forEach((String modifierBitName, List<String> keyNames) {
|
||||
@@ -61,35 +61,49 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
final String? secondaryLogicalName = keyNames.length == 3 ? keyNames[2] : null;
|
||||
final PhysicalKeyEntry primaryPhysical = physicalData.entryByName(primaryPhysicalName);
|
||||
final LogicalKeyEntry primaryLogical = logicalData.entryByName(primaryLogicalName);
|
||||
final LogicalKeyEntry? secondaryLogical = secondaryLogicalName == null ? null : logicalData.entryByName(secondaryLogicalName);
|
||||
final LogicalKeyEntry? secondaryLogical =
|
||||
secondaryLogicalName == null ? null : logicalData.entryByName(secondaryLogicalName);
|
||||
if (secondaryLogical == null && secondaryLogicalName != null) {
|
||||
print('Unrecognized secondary logical key $secondaryLogicalName specified for $debugFunctionName.');
|
||||
print(
|
||||
'Unrecognized secondary logical key $secondaryLogicalName specified for $debugFunctionName.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final String pad = secondaryLogical == null ? '' : ' ';
|
||||
result.writeln('''
|
||||
result.writeln(
|
||||
'''
|
||||
|
||||
data = g_new(FlKeyEmbedderCheckedKey, 1);
|
||||
g_hash_table_insert(table, GUINT_TO_POINTER(GDK_${modifierBitName}_MASK), data);
|
||||
data->is_caps_lock = ${primaryPhysicalName == 'CapsLock' ? 'true' : 'false'};
|
||||
data->primary_physical_key = ${toHex(primaryPhysical.usbHidCode, digits: 9)};$pad // ${primaryPhysical.constantName}
|
||||
data->primary_logical_key = ${toHex(primaryLogical.value, digits: 11)};$pad // ${primaryLogical.constantName}''');
|
||||
data->primary_logical_key = ${toHex(primaryLogical.value, digits: 11)};$pad // ${primaryLogical.constantName}''',
|
||||
);
|
||||
if (secondaryLogical != null) {
|
||||
result.writeln('''
|
||||
data->secondary_logical_key = ${toHex(secondaryLogical.value, digits: 11)}; // ${secondaryLogical.constantName}''');
|
||||
result.writeln(
|
||||
'''
|
||||
data->secondary_logical_key = ${toHex(secondaryLogical.value, digits: 11)}; // ${secondaryLogical.constantName}''',
|
||||
);
|
||||
}
|
||||
});
|
||||
return result.toString().trimRight();
|
||||
}
|
||||
|
||||
String get _gtkModifierBitMap {
|
||||
return constructMapFromModToKeys(_modifierBitMapping, keyData, logicalData, 'gtkModifierBitMap');
|
||||
return constructMapFromModToKeys(
|
||||
_modifierBitMapping,
|
||||
keyData,
|
||||
logicalData,
|
||||
'gtkModifierBitMap',
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, List<String>> _modifierBitMapping;
|
||||
|
||||
String get _gtkModeBitMap {
|
||||
return constructMapFromModToKeys(_lockBitMapping, keyData, logicalData, 'gtkModeBitMap');
|
||||
}
|
||||
|
||||
final Map<String, List<String>> _lockBitMapping;
|
||||
|
||||
final Map<String, bool> _layoutGoals;
|
||||
@@ -98,14 +112,17 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
_layoutGoals.forEach((String name, bool mandatory) {
|
||||
final PhysicalKeyEntry physicalEntry = keyData.entryByName(name);
|
||||
final LogicalKeyEntry logicalEntry = logicalData.entryByName(name);
|
||||
final String line = 'LayoutGoal{'
|
||||
final String line =
|
||||
'LayoutGoal{'
|
||||
'${toHex(physicalEntry.xKbScanCode, digits: 2)}, '
|
||||
'${toHex(logicalEntry.value, digits: 2)}, '
|
||||
'${mandatory ? 'true' : 'false'}'
|
||||
'},';
|
||||
lines.add(logicalEntry.value,
|
||||
' ${line.padRight(39)}'
|
||||
'// ${logicalEntry.name}');
|
||||
lines.add(
|
||||
logicalEntry.value,
|
||||
' ${line.padRight(39)}'
|
||||
'// ${logicalEntry.name}',
|
||||
);
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
@@ -113,13 +130,11 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
/// This generates the mask values for the part of a key code that defines its plane.
|
||||
String get _maskConstants {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
const List<MaskConstant> maskConstants = <MaskConstant>[
|
||||
kValueMask,
|
||||
kUnicodePlane,
|
||||
kGtkPlane,
|
||||
];
|
||||
const List<MaskConstant> maskConstants = <MaskConstant>[kValueMask, kUnicodePlane, kGtkPlane];
|
||||
for (final MaskConstant constant in maskConstants) {
|
||||
buffer.writeln('const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};');
|
||||
buffer.writeln(
|
||||
'const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};',
|
||||
);
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
@@ -128,8 +143,8 @@ class GtkCodeGenerator extends PlatformCodeGenerator {
|
||||
String get templatePath => path.join(dataRoot, 'gtk_key_mapping_cc.tmpl');
|
||||
|
||||
@override
|
||||
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
|
||||
'shell', 'platform', 'linux', 'key_mapping.g.cc');
|
||||
String outputPath(String platform) =>
|
||||
path.join(PlatformCodeGenerator.engineRoot, 'shell', 'platform', 'linux', 'key_mapping.g.cc');
|
||||
|
||||
@override
|
||||
Map<String, String> mappings() {
|
||||
|
||||
@@ -36,8 +36,10 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('iOS scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.iOSScanCode != null) {
|
||||
lines.add(entry.iOSScanCode!,
|
||||
' {${toHex(entry.iOSScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}');
|
||||
lines.add(
|
||||
entry.iOSScanCode!,
|
||||
' {${toHex(entry.iOSScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -62,7 +64,10 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('iOS keycode map');
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
zipStrict(entry.iOSKeyCodeValues, entry.iOSKeyCodeNames, (int iOSValue, String iOSName) {
|
||||
lines.add(iOSValue, ' {${toHex(iOSValue)}, ${toHex(entry.value, digits: 11)}}, // $iOSName');
|
||||
lines.add(
|
||||
iOSValue,
|
||||
' {${toHex(iOSValue)}, ${toHex(entry.value, digits: 11)}}, // $iOSName',
|
||||
);
|
||||
});
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -71,16 +76,14 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
/// This generates the mask values for the part of a key code that defines its plane.
|
||||
String get _maskConstants {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
const List<MaskConstant> maskConstants = <MaskConstant>[
|
||||
kValueMask,
|
||||
kUnicodePlane,
|
||||
kIosPlane,
|
||||
];
|
||||
const List<MaskConstant> maskConstants = <MaskConstant>[kValueMask, kUnicodePlane, kIosPlane];
|
||||
for (final MaskConstant constant in maskConstants) {
|
||||
buffer.writeln('/**');
|
||||
buffer.write(wrapString(constant.description, prefix: ' * '));
|
||||
buffer.writeln(' */');
|
||||
buffer.writeln('const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};');
|
||||
buffer.writeln(
|
||||
'const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};',
|
||||
);
|
||||
buffer.writeln();
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
@@ -90,7 +93,8 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _keyToModifierFlagMap {
|
||||
final StringBuffer modifierKeyMap = StringBuffer();
|
||||
for (final String name in kModifiersOfInterest) {
|
||||
final String line = '{${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}, kModifierFlag${lowerCamelToUpperCamel(name)}},';
|
||||
final String line =
|
||||
'{${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}, kModifierFlag${lowerCamelToUpperCamel(name)}},';
|
||||
modifierKeyMap.writeln(' ${line.padRight(42)}// $name');
|
||||
}
|
||||
return modifierKeyMap.toString().trimRight();
|
||||
@@ -100,7 +104,8 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _modifierFlagToKeyMap {
|
||||
final StringBuffer modifierKeyMap = StringBuffer();
|
||||
for (final String name in kModifiersOfInterest) {
|
||||
final String line = '{kModifierFlag${lowerCamelToUpperCamel(name)}, ${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}},';
|
||||
final String line =
|
||||
'{kModifierFlag${lowerCamelToUpperCamel(name)}, ${toHex(logicalData.entryByName(name).iOSKeyCodeValues[0])}},';
|
||||
modifierKeyMap.writeln(' ${line.padRight(42)}// $name');
|
||||
}
|
||||
return modifierKeyMap.toString().trimRight();
|
||||
@@ -119,10 +124,14 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _specialKeyConstants {
|
||||
final StringBuffer specialKeyConstants = StringBuffer();
|
||||
for (final String keyName in kSpecialPhysicalKeys) {
|
||||
specialKeyConstants.writeln('const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};');
|
||||
specialKeyConstants.writeln(
|
||||
'const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};',
|
||||
);
|
||||
}
|
||||
for (final String keyName in kSpecialLogicalKeys) {
|
||||
specialKeyConstants.writeln('const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};');
|
||||
specialKeyConstants.writeln(
|
||||
'const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};',
|
||||
);
|
||||
}
|
||||
return specialKeyConstants.toString().trimRight();
|
||||
}
|
||||
@@ -131,8 +140,16 @@ class IOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get templatePath => path.join(dataRoot, 'ios_key_code_map_mm.tmpl');
|
||||
|
||||
@override
|
||||
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
|
||||
'shell', 'platform', 'darwin', 'ios', 'framework', 'Source', 'KeyCodeMap.g.mm');
|
||||
String outputPath(String platform) => path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'shell',
|
||||
'platform',
|
||||
'darwin',
|
||||
'ios',
|
||||
'framework',
|
||||
'Source',
|
||||
'KeyCodeMap.g.mm',
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, String> mappings() {
|
||||
|
||||
@@ -55,10 +55,14 @@ class KeyboardKeysCodeGenerator extends BaseCodeGenerator {
|
||||
String get _physicalDefinitions {
|
||||
final OutputLines<int> lines = OutputLines<int>('Physical Key Definition');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
final String firstComment = _wrapString('Represents the location of the '
|
||||
'"${entry.commentName}" key on a generalized keyboard.');
|
||||
final String otherComments = _wrapString('See the function '
|
||||
'[RawKeyEvent.physicalKey] for more information.');
|
||||
final String firstComment = _wrapString(
|
||||
'Represents the location of the '
|
||||
'"${entry.commentName}" key on a generalized keyboard.',
|
||||
);
|
||||
final String otherComments = _wrapString(
|
||||
'See the function '
|
||||
'[RawKeyEvent.physicalKey] for more information.',
|
||||
);
|
||||
lines.add(entry.usbHidCode, '''
|
||||
$firstComment ///
|
||||
$otherComments static const PhysicalKeyboardKey ${entry.constantName} = PhysicalKeyboardKey(${toHex(entry.usbHidCode)});
|
||||
@@ -78,10 +82,17 @@ $otherComments static const PhysicalKeyboardKey ${entry.constantName} = Physica
|
||||
|
||||
/// Gets the generated definitions of LogicalKeyboardKeys.
|
||||
String get _logicalDefinitions {
|
||||
final OutputLines<int> lines = OutputLines<int>('Logical debug names', behavior: DeduplicateBehavior.kSkip);
|
||||
final OutputLines<int> lines = OutputLines<int>(
|
||||
'Logical debug names',
|
||||
behavior: DeduplicateBehavior.kSkip,
|
||||
);
|
||||
void printKey(int flutterId, String constantName, String commentName, {String? otherComments}) {
|
||||
final String firstComment = _wrapString('Represents the logical "$commentName" key on the keyboard.');
|
||||
otherComments ??= _wrapString('See the function [RawKeyEvent.logicalKey] for more information.');
|
||||
final String firstComment = _wrapString(
|
||||
'Represents the logical "$commentName" key on the keyboard.',
|
||||
);
|
||||
otherComments ??= _wrapString(
|
||||
'See the function [RawKeyEvent.logicalKey] for more information.',
|
||||
);
|
||||
lines.add(flutterId, '''
|
||||
$firstComment ///
|
||||
$otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardKey(${toHex(flutterId, digits: 11)});
|
||||
@@ -101,11 +112,13 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
|
||||
|
||||
String? _otherComments(String name) {
|
||||
if (synonyms.containsKey(name)) {
|
||||
final Set<String> unionNames = synonyms[name]!.keys.map(
|
||||
(LogicalKeyEntry entry) => entry.constantName).toSet();
|
||||
return _wrapString('This key represents the union of the keys '
|
||||
'$unionNames when comparing keys. This key will never be generated '
|
||||
'directly, its main use is in defining key maps.');
|
||||
final Set<String> unionNames =
|
||||
synonyms[name]!.keys.map((LogicalKeyEntry entry) => entry.constantName).toSet();
|
||||
return _wrapString(
|
||||
'This key represents the union of the keys '
|
||||
'$unionNames when comparing keys. This key will never be generated '
|
||||
'directly, its main use is in defining key maps.',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -125,14 +138,18 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
|
||||
final StringBuffer result = StringBuffer();
|
||||
for (final SynonymKeyInfo synonymInfo in synonyms.values) {
|
||||
final LogicalKeyEntry synonym = logicalData.entryByName(synonymInfo.name);
|
||||
final List<String> entries = synonymInfo.keys.map<String>((LogicalKeyEntry entry) => entry.constantName).toList();
|
||||
final List<String> entries =
|
||||
synonymInfo.keys.map<String>((LogicalKeyEntry entry) => entry.constantName).toList();
|
||||
result.writeln(' ${synonym.constantName}: <LogicalKeyboardKey>{${entries.join(', ')}},');
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
String get _logicalKeyLabels {
|
||||
final OutputLines<int> lines = OutputLines<int>('Logical key labels', behavior: DeduplicateBehavior.kSkip);
|
||||
final OutputLines<int> lines = OutputLines<int>(
|
||||
'Logical key labels',
|
||||
behavior: DeduplicateBehavior.kSkip,
|
||||
);
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
lines.add(entry.value, '''
|
||||
${toHex(entry.value, digits: 11)}: '${entry.commentName}',''');
|
||||
@@ -151,7 +168,10 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
|
||||
|
||||
/// This generates the map of Flutter key codes to logical keys.
|
||||
String get _predefinedKeyCodeMap {
|
||||
final OutputLines<int> lines = OutputLines<int>('Logical key map', behavior: DeduplicateBehavior.kSkip);
|
||||
final OutputLines<int> lines = OutputLines<int>(
|
||||
'Logical key map',
|
||||
behavior: DeduplicateBehavior.kSkip,
|
||||
);
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
lines.add(entry.value, ' ${toHex(entry.value, digits: 11)}: ${entry.constantName},');
|
||||
}
|
||||
@@ -159,7 +179,10 @@ $otherComments static const LogicalKeyboardKey $constantName = LogicalKeyboardK
|
||||
}
|
||||
|
||||
String get _maskConstantVariables {
|
||||
final OutputLines<int> lines = OutputLines<int>('Mask constants', behavior: DeduplicateBehavior.kKeep);
|
||||
final OutputLines<int> lines = OutputLines<int>(
|
||||
'Mask constants',
|
||||
behavior: DeduplicateBehavior.kKeep,
|
||||
);
|
||||
for (final MaskConstant constant in _maskConstants) {
|
||||
lines.add(constant.value, '''
|
||||
${_wrapString(constant.description)} ///
|
||||
@@ -190,14 +213,11 @@ ${_wrapString(constant.description)} ///
|
||||
|
||||
late final Map<String, SynonymKeyInfo> synonyms = Map<String, SynonymKeyInfo>.fromEntries(
|
||||
LogicalKeyData.synonyms.entries.map((MapEntry<String, List<String>> synonymDefinition) {
|
||||
final List<LogicalKeyEntry> entries = synonymDefinition.value.map(
|
||||
(String name) => logicalData.entryByName(name)).toList();
|
||||
final List<LogicalKeyEntry> entries =
|
||||
synonymDefinition.value.map((String name) => logicalData.entryByName(name)).toList();
|
||||
return MapEntry<String, SynonymKeyInfo>(
|
||||
synonymDefinition.key,
|
||||
SynonymKeyInfo(
|
||||
entries,
|
||||
synonymDefinition.key,
|
||||
),
|
||||
SynonymKeyInfo(entries, synonymDefinition.key),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -22,8 +22,8 @@ bool _isAsciiLetter(String? char) {
|
||||
const int charLowerZ = 0x7A;
|
||||
assert(char.length == 1);
|
||||
final int charCode = char.codeUnitAt(0);
|
||||
return (charCode >= charUpperA && charCode <= charUpperZ)
|
||||
|| (charCode >= charLowerA && charCode <= charLowerZ);
|
||||
return (charCode >= charUpperA && charCode <= charUpperZ) ||
|
||||
(charCode >= charLowerA && charCode <= charLowerZ);
|
||||
}
|
||||
|
||||
bool _isDigit(String? char) {
|
||||
@@ -44,7 +44,8 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
|
||||
List<PhysicalKeyEntry> get _numpadKeyData {
|
||||
return keyData.entries.where((PhysicalKeyEntry entry) {
|
||||
return entry.constantName.startsWith('numpad') && LogicalKeyData.printable.containsKey(entry.name);
|
||||
return entry.constantName.startsWith('numpad') &&
|
||||
LogicalKeyData.printable.containsKey(entry.name);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -57,7 +58,8 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
|
||||
List<LogicalKeyEntry> get _numpadLogicalKeyData {
|
||||
return logicalData.entries.where((LogicalKeyEntry entry) {
|
||||
return entry.constantName.startsWith('numpad') && LogicalKeyData.printable.containsKey(entry.name);
|
||||
return entry.constantName.startsWith('numpad') &&
|
||||
LogicalKeyData.printable.containsKey(entry.name);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -111,8 +113,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('GTK scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.xKbScanCode != null) {
|
||||
lines.add(entry.xKbScanCode!,
|
||||
' ${toHex(entry.xKbScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.xKbScanCode!,
|
||||
' ${toHex(entry.xKbScanCode)}: PhysicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -156,7 +160,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('Windows scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.windowsScanCode != null) {
|
||||
lines.add(entry.windowsScanCode!, ' ${entry.windowsScanCode}: PhysicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.windowsScanCode!,
|
||||
' ${entry.windowsScanCode}: PhysicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -179,11 +186,14 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
// Letter keys on Windows are not recorded in logical_key_data.g.json,
|
||||
// because they are not used by the embedding. Add them manually.
|
||||
final List<int>? keyCodes = entry.windowsValues.isNotEmpty
|
||||
? entry.windowsValues
|
||||
: (_isAsciiLetter(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
|
||||
_isDigit(entry.keyLabel) ? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)] :
|
||||
null);
|
||||
final List<int>? keyCodes =
|
||||
entry.windowsValues.isNotEmpty
|
||||
? entry.windowsValues
|
||||
: (_isAsciiLetter(entry.keyLabel)
|
||||
? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)]
|
||||
: _isDigit(entry.keyLabel)
|
||||
? <int>[entry.keyLabel!.toUpperCase().codeUnitAt(0)]
|
||||
: null);
|
||||
if (keyCodes != null) {
|
||||
for (final int code in keyCodes) {
|
||||
lines.add(code, ' $code: LogicalKeyboardKey.${entry.constantName},');
|
||||
@@ -198,7 +208,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('macOS scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.macOSScanCode != null) {
|
||||
lines.add(entry.macOSScanCode!, ' ${toHex(entry.macOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.macOSScanCode!,
|
||||
' ${toHex(entry.macOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -209,7 +222,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('macOS numpad map');
|
||||
for (final PhysicalKeyEntry entry in _numpadKeyData) {
|
||||
if (entry.macOSScanCode != null) {
|
||||
lines.add(entry.macOSScanCode!, ' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.macOSScanCode!,
|
||||
' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -219,7 +235,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('macOS function key map');
|
||||
for (final PhysicalKeyEntry entry in _functionKeyData) {
|
||||
if (entry.macOSScanCode != null) {
|
||||
lines.add(entry.macOSScanCode!, ' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.macOSScanCode!,
|
||||
' ${toHex(entry.macOSScanCode)}: LogicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -241,7 +260,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('iOS scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.iOSScanCode != null) {
|
||||
lines.add(entry.iOSScanCode!, ' ${toHex(entry.iOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.iOSScanCode!,
|
||||
' ${toHex(entry.iOSScanCode)}: PhysicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -262,7 +284,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('iOS numpad map');
|
||||
for (final PhysicalKeyEntry entry in _numpadKeyData) {
|
||||
if (entry.iOSScanCode != null) {
|
||||
lines.add(entry.iOSScanCode!,' ${toHex(entry.iOSScanCode)}: LogicalKeyboardKey.${entry.constantName},');
|
||||
lines.add(
|
||||
entry.iOSScanCode!,
|
||||
' ${toHex(entry.iOSScanCode)}: LogicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -294,7 +319,9 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
String get _fuchsiaHidCodeMap {
|
||||
final StringBuffer fuchsiaScanCodeMap = StringBuffer();
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
fuchsiaScanCodeMap.writeln(' ${toHex(entry.usbHidCode)}: PhysicalKeyboardKey.${entry.constantName},');
|
||||
fuchsiaScanCodeMap.writeln(
|
||||
' ${toHex(entry.usbHidCode)}: PhysicalKeyboardKey.${entry.constantName},',
|
||||
);
|
||||
}
|
||||
return fuchsiaScanCodeMap.toString().trimRight();
|
||||
}
|
||||
@@ -312,7 +339,10 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
|
||||
/// This generates the map of Web KeyboardEvent codes to physical keys.
|
||||
String get _webPhysicalKeyMap {
|
||||
final OutputLines<String> lines = OutputLines<String>('Web physical key map', behavior: DeduplicateBehavior.kKeep);
|
||||
final OutputLines<String> lines = OutputLines<String>(
|
||||
'Web physical key map',
|
||||
behavior: DeduplicateBehavior.kKeep,
|
||||
);
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
for (final String webCodes in entry.webCodes()) {
|
||||
lines.add(entry.name, " '$webCodes': PhysicalKeyboardKey.${entry.constantName},");
|
||||
@@ -331,14 +361,18 @@ class KeyboardMapsCodeGenerator extends BaseCodeGenerator {
|
||||
|
||||
/// This generates the map of Web number pad codes to logical keys.
|
||||
String get _webLocationMap {
|
||||
final String jsonRaw = File(path.join(dataRoot, 'web_logical_location_mapping.json')).readAsStringSync();
|
||||
final String jsonRaw =
|
||||
File(path.join(dataRoot, 'web_logical_location_mapping.json')).readAsStringSync();
|
||||
final Map<String, List<String?>> locationMap = parseMapOfListOfNullableString(jsonRaw);
|
||||
final OutputLines<String> lines = OutputLines<String>('Web location map');
|
||||
locationMap.forEach((String key, List<String?> keyNames) {
|
||||
final String keyStrings = keyNames.map((String? keyName) {
|
||||
final String? constantName = keyName == null ? null : logicalData.entryByName(keyName).constantName;
|
||||
return constantName != null ? 'LogicalKeyboardKey.$constantName' : 'null';
|
||||
}).join(', ');
|
||||
final String keyStrings = keyNames
|
||||
.map((String? keyName) {
|
||||
final String? constantName =
|
||||
keyName == null ? null : logicalData.entryByName(keyName).constantName;
|
||||
return constantName != null ? 'LogicalKeyboardKey.$constantName' : 'null';
|
||||
})
|
||||
.join(', ');
|
||||
lines.add(key, " '$key': <LogicalKeyboardKey?>[$keyStrings],");
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
|
||||
@@ -62,10 +62,11 @@ class LogicalKeyData {
|
||||
_readFuchsiaKeyCodes(data, physicalKeyData);
|
||||
_readGlfwKeyCodes(data, glfwHeaderFile, parseMapOfListOfString(glfwNameMap));
|
||||
// Sort entries by value
|
||||
final List<MapEntry<String, LogicalKeyEntry>> sortedEntries = data.entries.toList()..sort(
|
||||
(MapEntry<String, LogicalKeyEntry> a, MapEntry<String, LogicalKeyEntry> b) =>
|
||||
LogicalKeyEntry.compareByValue(a.value, b.value),
|
||||
);
|
||||
final List<MapEntry<String, LogicalKeyEntry>> sortedEntries =
|
||||
data.entries.toList()..sort(
|
||||
(MapEntry<String, LogicalKeyEntry> a, MapEntry<String, LogicalKeyEntry> b) =>
|
||||
LogicalKeyEntry.compareByValue(a.value, b.value),
|
||||
);
|
||||
data
|
||||
..clear()
|
||||
..addEntries(sortedEntries);
|
||||
@@ -75,10 +76,14 @@ class LogicalKeyData {
|
||||
/// Parses the given JSON data and populates the data structure from it.
|
||||
factory LogicalKeyData.fromJson(Map<String, dynamic> contentMap) {
|
||||
final Map<String, LogicalKeyEntry> data = <String, LogicalKeyEntry>{};
|
||||
data.addEntries(contentMap.values.map((dynamic value) {
|
||||
final LogicalKeyEntry entry = LogicalKeyEntry.fromJsonMapEntry(value as Map<String, dynamic>);
|
||||
return MapEntry<String, LogicalKeyEntry>(entry.name, entry);
|
||||
}));
|
||||
data.addEntries(
|
||||
contentMap.values.map((dynamic value) {
|
||||
final LogicalKeyEntry entry = LogicalKeyEntry.fromJsonMapEntry(
|
||||
value as Map<String, dynamic>,
|
||||
);
|
||||
return MapEntry<String, LogicalKeyEntry>(entry.name, entry);
|
||||
}),
|
||||
);
|
||||
return LogicalKeyData._(data);
|
||||
}
|
||||
|
||||
@@ -102,8 +107,7 @@ class LogicalKeyData {
|
||||
///
|
||||
/// Asserts if the name is not found.
|
||||
LogicalKeyEntry entryByName(String name) {
|
||||
assert(_data.containsKey(name),
|
||||
'Unable to find logical entry by name $name.');
|
||||
assert(_data.containsKey(name), 'Unable to find logical entry by name $name.');
|
||||
return _data[name]!;
|
||||
}
|
||||
|
||||
@@ -150,12 +154,17 @@ class LogicalKeyData {
|
||||
if (webName.startsWith('.')) {
|
||||
continue;
|
||||
}
|
||||
final String name = LogicalKeyEntry.computeName(webName.replaceAll(RegExp('[^A-Za-z0-9]'), ''));
|
||||
final int value = match.namedGroup('unicode') != null ?
|
||||
getHex(match.namedGroup('unicode')!) :
|
||||
match.namedGroup('char')!.codeUnitAt(0);
|
||||
final String? keyLabel = (match.namedGroup('kind')! == 'UNI' && !_isControlCharacter(value)) ?
|
||||
String.fromCharCode(value) : null;
|
||||
final String name = LogicalKeyEntry.computeName(
|
||||
webName.replaceAll(RegExp('[^A-Za-z0-9]'), ''),
|
||||
);
|
||||
final int value =
|
||||
match.namedGroup('unicode') != null
|
||||
? getHex(match.namedGroup('unicode')!)
|
||||
: match.namedGroup('char')!.codeUnitAt(0);
|
||||
final String? keyLabel =
|
||||
(match.namedGroup('kind')! == 'UNI' && !_isControlCharacter(value))
|
||||
? String.fromCharCode(value)
|
||||
: null;
|
||||
// Skip modifier keys from DOM. They will be added with supplemental data.
|
||||
if (_chromeModifiers.containsKey(name) && source == 'DOM') {
|
||||
continue;
|
||||
@@ -163,20 +172,17 @@ class LogicalKeyData {
|
||||
|
||||
final bool isPrintable = keyLabel != null;
|
||||
final int entryValue = toPlane(value, _sourceToPlane(source, isPrintable));
|
||||
final LogicalKeyEntry entry = dataByValue.putIfAbsent(entryValue, () =>
|
||||
LogicalKeyEntry.fromName(
|
||||
value: entryValue,
|
||||
name: name,
|
||||
keyLabel: keyLabel,
|
||||
),
|
||||
final LogicalKeyEntry entry = dataByValue.putIfAbsent(
|
||||
entryValue,
|
||||
() => LogicalKeyEntry.fromName(value: entryValue, name: name, keyLabel: keyLabel),
|
||||
);
|
||||
if (source == 'DOM' && !isPrintable) {
|
||||
entry.webNames.add(webName);
|
||||
}
|
||||
}
|
||||
return Map<String, LogicalKeyEntry>.fromEntries(
|
||||
dataByValue.values.map((LogicalKeyEntry entry) =>
|
||||
MapEntry<String, LogicalKeyEntry>(entry.name, entry),
|
||||
dataByValue.values.map(
|
||||
(LogicalKeyEntry entry) => MapEntry<String, LogicalKeyEntry>(entry.name, entry),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -186,16 +192,21 @@ class LogicalKeyData {
|
||||
PhysicalKeyData physicalKeyData,
|
||||
Map<String, List<String>> logicalToPhysical,
|
||||
) {
|
||||
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical,
|
||||
(String logicalKeyName, String physicalKeyName) { print('Duplicate logical key name $logicalKeyName for macOS'); });
|
||||
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical, (
|
||||
String logicalKeyName,
|
||||
String physicalKeyName,
|
||||
) {
|
||||
print('Duplicate logical key name $logicalKeyName for macOS');
|
||||
});
|
||||
|
||||
physicalToLogical.forEach((String physicalKeyName, String logicalKeyName) {
|
||||
final PhysicalKeyEntry physicalEntry = physicalKeyData.entryByName(physicalKeyName);
|
||||
assert(physicalEntry.macOSScanCode != null,
|
||||
'Physical entry $physicalKeyName does not have a macOSScanCode.');
|
||||
assert(
|
||||
physicalEntry.macOSScanCode != null,
|
||||
'Physical entry $physicalKeyName does not have a macOSScanCode.',
|
||||
);
|
||||
final LogicalKeyEntry? logicalEntry = data[logicalKeyName];
|
||||
assert(logicalEntry != null,
|
||||
'Unable to find logical entry by name $logicalKeyName.');
|
||||
assert(logicalEntry != null, 'Unable to find logical entry by name $logicalKeyName.');
|
||||
logicalEntry!.macOSKeyCodeNames.add(physicalEntry.name);
|
||||
logicalEntry.macOSKeyCodeValues.add(physicalEntry.macOSScanCode!);
|
||||
});
|
||||
@@ -206,16 +217,21 @@ class LogicalKeyData {
|
||||
PhysicalKeyData physicalKeyData,
|
||||
Map<String, List<String>> logicalToPhysical,
|
||||
) {
|
||||
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical,
|
||||
(String logicalKeyName, String physicalKeyName) { print('Duplicate logical key name $logicalKeyName for iOS'); });
|
||||
final Map<String, String> physicalToLogical = reverseMapOfListOfString(logicalToPhysical, (
|
||||
String logicalKeyName,
|
||||
String physicalKeyName,
|
||||
) {
|
||||
print('Duplicate logical key name $logicalKeyName for iOS');
|
||||
});
|
||||
|
||||
physicalToLogical.forEach((String physicalKeyName, String logicalKeyName) {
|
||||
final PhysicalKeyEntry physicalEntry = physicalKeyData.entryByName(physicalKeyName);
|
||||
assert(physicalEntry.iOSScanCode != null,
|
||||
'Physical entry $physicalKeyName does not have an iosScanCode.');
|
||||
assert(
|
||||
physicalEntry.iOSScanCode != null,
|
||||
'Physical entry $physicalKeyName does not have an iosScanCode.',
|
||||
);
|
||||
final LogicalKeyEntry? logicalEntry = data[logicalKeyName];
|
||||
assert(logicalEntry != null,
|
||||
'Unable to find logical entry by name $logicalKeyName.');
|
||||
assert(logicalEntry != null, 'Unable to find logical entry by name $logicalKeyName.');
|
||||
logicalEntry!.iOSKeyCodeNames.add(physicalEntry.name);
|
||||
logicalEntry.iOSKeyCodeValues.add(physicalEntry.iOSScanCode!);
|
||||
});
|
||||
@@ -226,14 +242,22 @@ class LogicalKeyData {
|
||||
/// Lines in this file look like this (without the ///):
|
||||
/// /** Space key. */
|
||||
/// #define GDK_KEY_space 0x020
|
||||
static void _readGtkKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameToGtkName) {
|
||||
static void _readGtkKeyCodes(
|
||||
Map<String, LogicalKeyEntry> data,
|
||||
String headerFile,
|
||||
Map<String, List<String>> nameToGtkName,
|
||||
) {
|
||||
final RegExp definedCodes = RegExp(
|
||||
r'#define '
|
||||
r'GDK_KEY_(?<name>[a-zA-Z0-9_]+)\s*'
|
||||
r'0x(?<value>[0-9a-f]+),?',
|
||||
);
|
||||
final Map<String, String> gtkNameToFlutterName = reverseMapOfListOfString(nameToGtkName,
|
||||
(String flutterName, String gtkName) { print('Duplicate GTK logical name $gtkName'); });
|
||||
final Map<String, String> gtkNameToFlutterName = reverseMapOfListOfString(nameToGtkName, (
|
||||
String flutterName,
|
||||
String gtkName,
|
||||
) {
|
||||
print('Duplicate GTK logical name $gtkName');
|
||||
});
|
||||
|
||||
for (final RegExpMatch match in definedCodes.allMatches(headerFile)) {
|
||||
final String gtkName = match.namedGroup('name')!;
|
||||
@@ -255,11 +279,19 @@ class LogicalKeyData {
|
||||
}
|
||||
}
|
||||
|
||||
static void _readWindowsKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameMap) {
|
||||
static void _readWindowsKeyCodes(
|
||||
Map<String, LogicalKeyEntry> data,
|
||||
String headerFile,
|
||||
Map<String, List<String>> nameMap,
|
||||
) {
|
||||
// The mapping from the Flutter name (e.g. "enter") to the Windows name (e.g.
|
||||
// "RETURN").
|
||||
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap,
|
||||
(String flutterName, String windowsName) { print('Duplicate Windows logical name $windowsName'); });
|
||||
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap, (
|
||||
String flutterName,
|
||||
String windowsName,
|
||||
) {
|
||||
print('Duplicate Windows logical name $windowsName');
|
||||
});
|
||||
|
||||
final RegExp definedCodes = RegExp(
|
||||
r'define '
|
||||
@@ -279,12 +311,7 @@ class LogicalKeyData {
|
||||
print('Invalid logical entry by name $name (from Windows $windowsName)');
|
||||
continue;
|
||||
}
|
||||
addNameValue(
|
||||
entry.windowsNames,
|
||||
entry.windowsValues,
|
||||
windowsName,
|
||||
value,
|
||||
);
|
||||
addNameValue(entry.windowsNames, entry.windowsValues, windowsName, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,9 +320,17 @@ class LogicalKeyData {
|
||||
/// Lines in this file look like this (without the ///):
|
||||
/// /** Left Control modifier key. */
|
||||
/// AKEYCODE_CTRL_LEFT = 113,
|
||||
static void _readAndroidKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameMap) {
|
||||
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap,
|
||||
(String flutterName, String androidName) { print('Duplicate Android logical name $androidName'); });
|
||||
static void _readAndroidKeyCodes(
|
||||
Map<String, LogicalKeyEntry> data,
|
||||
String headerFile,
|
||||
Map<String, List<String>> nameMap,
|
||||
) {
|
||||
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap, (
|
||||
String flutterName,
|
||||
String androidName,
|
||||
) {
|
||||
print('Duplicate Android logical name $androidName');
|
||||
});
|
||||
|
||||
final RegExp enumBlock = RegExp(r'enum\s*\{(.*)\};', multiLine: true);
|
||||
// Eliminate everything outside of the enum block.
|
||||
@@ -324,22 +359,26 @@ class LogicalKeyData {
|
||||
}
|
||||
}
|
||||
|
||||
static void _readFuchsiaKeyCodes(Map<String, LogicalKeyEntry> data, PhysicalKeyData physicalData) {
|
||||
static void _readFuchsiaKeyCodes(
|
||||
Map<String, LogicalKeyEntry> data,
|
||||
PhysicalKeyData physicalData,
|
||||
) {
|
||||
for (final LogicalKeyEntry entry in data.values) {
|
||||
final int? value = (() {
|
||||
if (entry.value == 0) {
|
||||
return 0;
|
||||
}
|
||||
final String? keyLabel = printable[entry.constantName];
|
||||
if (keyLabel != null && !entry.constantName.startsWith('numpad')) {
|
||||
return toPlane(keyLabel.codeUnitAt(0), kUnicodePlane.value);
|
||||
} else {
|
||||
final PhysicalKeyEntry? physicalEntry = physicalData.tryEntryByName(entry.name);
|
||||
if (physicalEntry != null) {
|
||||
return toPlane(physicalEntry.usbHidCode, kFuchsiaPlane.value);
|
||||
}
|
||||
}
|
||||
})();
|
||||
final int? value =
|
||||
(() {
|
||||
if (entry.value == 0) {
|
||||
return 0;
|
||||
}
|
||||
final String? keyLabel = printable[entry.constantName];
|
||||
if (keyLabel != null && !entry.constantName.startsWith('numpad')) {
|
||||
return toPlane(keyLabel.codeUnitAt(0), kUnicodePlane.value);
|
||||
} else {
|
||||
final PhysicalKeyEntry? physicalEntry = physicalData.tryEntryByName(entry.name);
|
||||
if (physicalEntry != null) {
|
||||
return toPlane(physicalEntry.usbHidCode, kFuchsiaPlane.value);
|
||||
}
|
||||
}
|
||||
})();
|
||||
if (value != null) {
|
||||
entry.fuchsiaValues.add(value);
|
||||
}
|
||||
@@ -352,9 +391,17 @@ class LogicalKeyData {
|
||||
/// /** Space key. */
|
||||
/// #define GLFW_KEY_SPACE 32,
|
||||
/// #define GLFW_KEY_LAST GLFW_KEY_MENU
|
||||
static void _readGlfwKeyCodes(Map<String, LogicalKeyEntry> data, String headerFile, Map<String, List<String>> nameMap) {
|
||||
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap,
|
||||
(String flutterName, String glfwName) { print('Duplicate GLFW logical name $glfwName'); });
|
||||
static void _readGlfwKeyCodes(
|
||||
Map<String, LogicalKeyEntry> data,
|
||||
String headerFile,
|
||||
Map<String, List<String>> nameMap,
|
||||
) {
|
||||
final Map<String, String> nameToFlutterName = reverseMapOfListOfString(nameMap, (
|
||||
String flutterName,
|
||||
String glfwName,
|
||||
) {
|
||||
print('Duplicate GLFW logical name $glfwName');
|
||||
});
|
||||
|
||||
// Only get the KEY definitions, ignore the rest (mouse, joystick, etc).
|
||||
final RegExp definedCodes = RegExp(
|
||||
@@ -388,47 +435,47 @@ class LogicalKeyData {
|
||||
print('Invalid logical entry by name $name (from GLFW $glfwName)');
|
||||
return;
|
||||
}
|
||||
addNameValue(
|
||||
entry.glfwNames,
|
||||
entry.glfwValues,
|
||||
glfwName,
|
||||
value,
|
||||
);
|
||||
addNameValue(entry.glfwNames, entry.glfwValues, glfwName, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Map Web key to the pair of key names
|
||||
static final Map<String, _ModifierPair> _chromeModifiers = () {
|
||||
final String rawJson = File(path.join(dataRoot, 'chromium_modifiers.json',)).readAsStringSync();
|
||||
final String rawJson = File(path.join(dataRoot, 'chromium_modifiers.json')).readAsStringSync();
|
||||
return (json.decode(rawJson) as Map<String, dynamic>).map((String key, dynamic value) {
|
||||
final List<dynamic> pair = value as List<dynamic>;
|
||||
return MapEntry<String, _ModifierPair>(key, _ModifierPair(pair[0] as String, pair[1] as String));
|
||||
return MapEntry<String, _ModifierPair>(
|
||||
key,
|
||||
_ModifierPair(pair[0] as String, pair[1] as String),
|
||||
);
|
||||
});
|
||||
}();
|
||||
|
||||
/// Returns the static map of printable representations.
|
||||
static final Map<String, String> printable = (() {
|
||||
final String printableKeys = File(path.join(dataRoot, 'printable.json',)).readAsStringSync();
|
||||
return (json.decode(printableKeys) as Map<String, dynamic>)
|
||||
.cast<String, String>();
|
||||
})();
|
||||
static final Map<String, String> printable =
|
||||
(() {
|
||||
final String printableKeys = File(path.join(dataRoot, 'printable.json')).readAsStringSync();
|
||||
return (json.decode(printableKeys) as Map<String, dynamic>).cast<String, String>();
|
||||
})();
|
||||
|
||||
/// Returns the static map of synonym representations.
|
||||
///
|
||||
/// These include synonyms for keys which don't have printable
|
||||
/// representations, and appear in more than one place on the keyboard (e.g.
|
||||
/// SHIFT, ALT, etc.).
|
||||
static final Map<String, List<String>> synonyms = (() {
|
||||
final String synonymKeys = File(path.join(dataRoot, 'synonyms.json',)).readAsStringSync();
|
||||
final Map<String, dynamic> dynamicSynonym = json.decode(synonymKeys) as Map<String, dynamic>;
|
||||
return dynamicSynonym.map((String name, dynamic values) {
|
||||
// The keygen and algorithm of macOS relies on synonyms being pairs.
|
||||
// See siblingKeyMap in macos_code_gen.dart.
|
||||
final List<String> names = (values as List<dynamic>).whereType<String>().toList();
|
||||
assert(names.length == 2);
|
||||
return MapEntry<String, List<String>>(name, names);
|
||||
});
|
||||
})();
|
||||
static final Map<String, List<String>> synonyms =
|
||||
(() {
|
||||
final String synonymKeys = File(path.join(dataRoot, 'synonyms.json')).readAsStringSync();
|
||||
final Map<String, dynamic> dynamicSynonym =
|
||||
json.decode(synonymKeys) as Map<String, dynamic>;
|
||||
return dynamicSynonym.map((String name, dynamic values) {
|
||||
// The keygen and algorithm of macOS relies on synonyms being pairs.
|
||||
// See siblingKeyMap in macos_code_gen.dart.
|
||||
final List<String> names = (values as List<dynamic>).whereType<String>().toList();
|
||||
assert(names.length == 2);
|
||||
return MapEntry<String, List<String>>(name, names);
|
||||
});
|
||||
})();
|
||||
|
||||
static int _sourceToPlane(String source, bool isPrintable) {
|
||||
if (isPrintable) {
|
||||
@@ -446,41 +493,30 @@ class LogicalKeyData {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A single entry in the key data structure.
|
||||
///
|
||||
/// Can be read from JSON with the [LogicalKeyEntry.fromJsonMapEntry] constructor, or
|
||||
/// written with the [toJson] method.
|
||||
class LogicalKeyEntry {
|
||||
/// Creates a single key entry from available data.
|
||||
LogicalKeyEntry({
|
||||
required this.value,
|
||||
required this.name,
|
||||
this.keyLabel,
|
||||
}) : webNames = <String>[],
|
||||
macOSKeyCodeNames = <String>[],
|
||||
macOSKeyCodeValues = <int>[],
|
||||
iOSKeyCodeNames = <String>[],
|
||||
iOSKeyCodeValues = <int>[],
|
||||
gtkNames = <String>[],
|
||||
gtkValues = <int>[],
|
||||
windowsNames = <String>[],
|
||||
windowsValues = <int>[],
|
||||
androidNames = <String>[],
|
||||
androidValues = <int>[],
|
||||
fuchsiaValues = <int>[],
|
||||
glfwNames = <String>[],
|
||||
glfwValues = <int>[];
|
||||
LogicalKeyEntry({required this.value, required this.name, this.keyLabel})
|
||||
: webNames = <String>[],
|
||||
macOSKeyCodeNames = <String>[],
|
||||
macOSKeyCodeValues = <int>[],
|
||||
iOSKeyCodeNames = <String>[],
|
||||
iOSKeyCodeValues = <int>[],
|
||||
gtkNames = <String>[],
|
||||
gtkValues = <int>[],
|
||||
windowsNames = <String>[],
|
||||
windowsValues = <int>[],
|
||||
androidNames = <String>[],
|
||||
androidValues = <int>[],
|
||||
fuchsiaValues = <int>[],
|
||||
glfwNames = <String>[],
|
||||
glfwValues = <int>[];
|
||||
|
||||
LogicalKeyEntry.fromName({
|
||||
required int value,
|
||||
required String name,
|
||||
String? keyLabel,
|
||||
}) : this(
|
||||
value: value,
|
||||
name: name,
|
||||
keyLabel: keyLabel,
|
||||
);
|
||||
LogicalKeyEntry.fromName({required int value, required String name, String? keyLabel})
|
||||
: this(value: value, name: name, keyLabel: keyLabel);
|
||||
|
||||
/// Populates the key from a JSON map.
|
||||
LogicalKeyEntry.fromJsonMapEntry(Map<String, dynamic> map)
|
||||
@@ -628,23 +664,37 @@ class LogicalKeyEntry {
|
||||
/// separate words (e.g. "wakeUp" converts to "Wake Up").
|
||||
static String computeCommentName(String name) {
|
||||
final String replaced = name.replaceAllMapped(
|
||||
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'), (Match match) => '${match.group(1)} ${match.group(2)}',
|
||||
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'),
|
||||
(Match match) => '${match.group(1)} ${match.group(2)}',
|
||||
);
|
||||
return replaced
|
||||
// 'fooBar' => 'foo Bar', 'fooBAR' => 'foo BAR'
|
||||
.replaceAllMapped(RegExp(r'([^A-Z])([A-Z])'), (Match match) => '${match.group(1)} ${match.group(2)}')
|
||||
// 'ABCDoo' => 'ABC Doo'
|
||||
.replaceAllMapped(RegExp(r'([A-Z])([A-Z])([a-z])'), (Match match) => '${match.group(1)} ${match.group(2)}${match.group(3)}')
|
||||
// 'AB1' => 'AB 1', 'F1' => 'F1'
|
||||
.replaceAllMapped(RegExp(r'([A-Z]{2,})([0-9])'), (Match match) => '${match.group(1)} ${match.group(2)}')
|
||||
// 'Foo1' => 'Foo 1'
|
||||
.replaceAllMapped(RegExp(r'([a-z])([0-9])'), (Match match) => '${match.group(1)} ${match.group(2)}')
|
||||
.trim();
|
||||
// 'fooBar' => 'foo Bar', 'fooBAR' => 'foo BAR'
|
||||
.replaceAllMapped(
|
||||
RegExp(r'([^A-Z])([A-Z])'),
|
||||
(Match match) => '${match.group(1)} ${match.group(2)}',
|
||||
)
|
||||
// 'ABCDoo' => 'ABC Doo'
|
||||
.replaceAllMapped(
|
||||
RegExp(r'([A-Z])([A-Z])([a-z])'),
|
||||
(Match match) => '${match.group(1)} ${match.group(2)}${match.group(3)}',
|
||||
)
|
||||
// 'AB1' => 'AB 1', 'F1' => 'F1'
|
||||
.replaceAllMapped(
|
||||
RegExp(r'([A-Z]{2,})([0-9])'),
|
||||
(Match match) => '${match.group(1)} ${match.group(2)}',
|
||||
)
|
||||
// 'Foo1' => 'Foo 1'
|
||||
.replaceAllMapped(
|
||||
RegExp(r'([a-z])([0-9])'),
|
||||
(Match match) => '${match.group(1)} ${match.group(2)}',
|
||||
)
|
||||
.trim();
|
||||
}
|
||||
|
||||
static String computeConstantName(String commentName) {
|
||||
// Convert the first word in the comment name.
|
||||
final String lowerCamelSpace = commentName.replaceFirstMapped(RegExp(r'^[^ ]+'),
|
||||
final String lowerCamelSpace = commentName.replaceFirstMapped(
|
||||
RegExp(r'^[^ ]+'),
|
||||
(Match match) => match[0]!.toLowerCase(),
|
||||
);
|
||||
final String result = lowerCamelSpace.replaceAll(' ', '');
|
||||
@@ -654,6 +704,5 @@ class LogicalKeyEntry {
|
||||
return result;
|
||||
}
|
||||
|
||||
static int compareByValue(LogicalKeyEntry a, LogicalKeyEntry b) =>
|
||||
a.value.compareTo(b.value);
|
||||
static int compareByValue(LogicalKeyEntry a, LogicalKeyEntry b) => a.value.compareTo(b.value);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
final OutputLines<int> lines = OutputLines<int>('macOS scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.macOSScanCode != null) {
|
||||
lines.add(entry.macOSScanCode!, ' @${toHex(entry.macOSScanCode)} : @${toHex(entry.usbHidCode)}, // ${entry.constantName}');
|
||||
lines.add(
|
||||
entry.macOSScanCode!,
|
||||
' @${toHex(entry.macOSScanCode)} : @${toHex(entry.usbHidCode)}, // ${entry.constantName}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -44,9 +47,14 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _keyCodeToLogicalMap {
|
||||
final OutputLines<int> lines = OutputLines<int>('macOS keycode map');
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
zipStrict(entry.macOSKeyCodeValues, entry.macOSKeyCodeNames, (int macOSValue, String macOSName) {
|
||||
lines.add(macOSValue,
|
||||
' @${toHex(macOSValue)} : @${toHex(entry.value, digits: 11)}, // $macOSName -> ${entry.constantName}');
|
||||
zipStrict(entry.macOSKeyCodeValues, entry.macOSKeyCodeNames, (
|
||||
int macOSValue,
|
||||
String macOSName,
|
||||
) {
|
||||
lines.add(
|
||||
macOSValue,
|
||||
' @${toHex(macOSValue)} : @${toHex(entry.value, digits: 11)}, // $macOSName -> ${entry.constantName}',
|
||||
);
|
||||
});
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -55,13 +63,11 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
/// This generates the mask values for the part of a key code that defines its plane.
|
||||
String get _maskConstants {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
const List<MaskConstant> maskConstants = <MaskConstant>[
|
||||
kValueMask,
|
||||
kUnicodePlane,
|
||||
kMacosPlane,
|
||||
];
|
||||
const List<MaskConstant> maskConstants = <MaskConstant>[kValueMask, kUnicodePlane, kMacosPlane];
|
||||
for (final MaskConstant constant in maskConstants) {
|
||||
buffer.writeln('const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};');
|
||||
buffer.writeln(
|
||||
'const uint64_t k${constant.upperCamelName} = ${toHex(constant.value, digits: 11)};',
|
||||
);
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
@@ -70,7 +76,9 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _keyToModifierFlagMap {
|
||||
final StringBuffer modifierKeyMap = StringBuffer();
|
||||
for (final String name in kModifiersOfInterest) {
|
||||
modifierKeyMap.writeln(' @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])} : @(kModifierFlag${lowerCamelToUpperCamel(name)}),');
|
||||
modifierKeyMap.writeln(
|
||||
' @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])} : @(kModifierFlag${lowerCamelToUpperCamel(name)}),',
|
||||
);
|
||||
}
|
||||
return modifierKeyMap.toString().trimRight();
|
||||
}
|
||||
@@ -79,7 +87,9 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _modifierFlagToKeyMap {
|
||||
final StringBuffer modifierKeyMap = StringBuffer();
|
||||
for (final String name in kModifiersOfInterest) {
|
||||
modifierKeyMap.writeln(' @(kModifierFlag${lowerCamelToUpperCamel(name)}) : @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])},');
|
||||
modifierKeyMap.writeln(
|
||||
' @(kModifierFlag${lowerCamelToUpperCamel(name)}) : @${toHex(logicalData.entryByName(name).macOSKeyCodeValues[0])},',
|
||||
);
|
||||
}
|
||||
return modifierKeyMap.toString().trimRight();
|
||||
}
|
||||
@@ -88,10 +98,14 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _specialKeyConstants {
|
||||
final StringBuffer specialKeyConstants = StringBuffer();
|
||||
for (final String keyName in kSpecialPhysicalKeys) {
|
||||
specialKeyConstants.writeln('const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};');
|
||||
specialKeyConstants.writeln(
|
||||
'const uint64_t k${keyName}PhysicalKey = ${toHex(keyData.entryByName(keyName).usbHidCode)};',
|
||||
);
|
||||
}
|
||||
for (final String keyName in kSpecialLogicalKeys) {
|
||||
specialKeyConstants.writeln('const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};');
|
||||
specialKeyConstants.writeln(
|
||||
'const uint64_t k${lowerCamelToUpperCamel(keyName)}LogicalKey = ${toHex(logicalData.entryByName(keyName).value)};',
|
||||
);
|
||||
}
|
||||
return specialKeyConstants.toString().trimRight();
|
||||
}
|
||||
@@ -102,14 +116,17 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
_layoutGoals.forEach((String name, bool mandatory) {
|
||||
final PhysicalKeyEntry physicalEntry = keyData.entryByName(name);
|
||||
final LogicalKeyEntry logicalEntry = logicalData.entryByName(name);
|
||||
final String line = 'LayoutGoal{'
|
||||
final String line =
|
||||
'LayoutGoal{'
|
||||
'${toHex(physicalEntry.macOSScanCode, digits: 2)}, '
|
||||
'${toHex(logicalEntry.value, digits: 2)}, '
|
||||
'${mandatory ? 'true' : 'false'}'
|
||||
'},';
|
||||
lines.add(logicalEntry.value,
|
||||
' ${line.padRight(32)}'
|
||||
'// ${logicalEntry.name}');
|
||||
lines.add(
|
||||
logicalEntry.value,
|
||||
' ${line.padRight(32)}'
|
||||
'// ${logicalEntry.name}',
|
||||
);
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
@@ -118,8 +135,16 @@ class MacOSCodeGenerator extends PlatformCodeGenerator {
|
||||
String get templatePath => path.join(dataRoot, 'macos_key_code_map_cc.tmpl');
|
||||
|
||||
@override
|
||||
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
|
||||
'shell', 'platform', 'darwin', 'macos', 'framework', 'Source', 'KeyCodeMap.g.mm');
|
||||
String outputPath(String platform) => path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'shell',
|
||||
'platform',
|
||||
'darwin',
|
||||
'macos',
|
||||
'framework',
|
||||
'Source',
|
||||
'KeyCodeMap.g.mm',
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, String> mappings() {
|
||||
|
||||
@@ -18,15 +18,19 @@ class PhysicalKeyData {
|
||||
String androidKeyboardLayout,
|
||||
String androidNameMap,
|
||||
) {
|
||||
final Map<String, List<int>> nameToAndroidScanCodes = _readAndroidScanCodes(androidKeyboardLayout, androidNameMap);
|
||||
final Map<String, List<int>> nameToAndroidScanCodes = _readAndroidScanCodes(
|
||||
androidKeyboardLayout,
|
||||
androidNameMap,
|
||||
);
|
||||
final Map<String, PhysicalKeyEntry> data = _readHidEntries(
|
||||
chromiumHidCodes,
|
||||
nameToAndroidScanCodes,
|
||||
);
|
||||
final List<MapEntry<String, PhysicalKeyEntry>> sortedEntries = data.entries.toList()..sort(
|
||||
(MapEntry<String, PhysicalKeyEntry> a, MapEntry<String, PhysicalKeyEntry> b) =>
|
||||
PhysicalKeyEntry.compareByUsbHidCode(a.value, b.value),
|
||||
);
|
||||
final List<MapEntry<String, PhysicalKeyEntry>> sortedEntries =
|
||||
data.entries.toList()..sort(
|
||||
(MapEntry<String, PhysicalKeyEntry> a, MapEntry<String, PhysicalKeyEntry> b) =>
|
||||
PhysicalKeyEntry.compareByUsbHidCode(a.value, b.value),
|
||||
);
|
||||
data
|
||||
..clear()
|
||||
..addEntries(sortedEntries);
|
||||
@@ -37,7 +41,9 @@ class PhysicalKeyData {
|
||||
factory PhysicalKeyData.fromJson(Map<String, dynamic> contentMap) {
|
||||
final Map<String, PhysicalKeyEntry> data = <String, PhysicalKeyEntry>{};
|
||||
for (final MapEntry<String, dynamic> jsonEntry in contentMap.entries) {
|
||||
final PhysicalKeyEntry entry = PhysicalKeyEntry.fromJsonMapEntry(jsonEntry.value as Map<String, dynamic>);
|
||||
final PhysicalKeyEntry entry = PhysicalKeyEntry.fromJsonMapEntry(
|
||||
jsonEntry.value as Map<String, dynamic>,
|
||||
);
|
||||
data[entry.name] = entry;
|
||||
}
|
||||
return PhysicalKeyData._(data);
|
||||
@@ -55,8 +61,7 @@ class PhysicalKeyData {
|
||||
/// Asserts if the name is not found.
|
||||
PhysicalKeyEntry entryByName(String name) {
|
||||
final PhysicalKeyEntry? entry = tryEntryByName(name);
|
||||
assert(entry != null,
|
||||
'Unable to find logical entry by name $name.');
|
||||
assert(entry != null, 'Unable to find logical entry by name $name.');
|
||||
return entry!;
|
||||
}
|
||||
|
||||
@@ -98,7 +103,7 @@ class PhysicalKeyData {
|
||||
r'key\s+' // Literal "key"
|
||||
r'(?<id>[0-9]+)\s*' // ID section
|
||||
r'"?(?:KEY_)?(?<name>[0-9A-Z_]+|\(undefined\))"?\s*' // Name section
|
||||
r'(?<function>FUNCTION)?' // Optional literal "FUNCTION"
|
||||
r'(?<function>FUNCTION)?', // Optional literal "FUNCTION"
|
||||
);
|
||||
final Map<String, List<int>> androidNameToScanCodes = <String, List<int>>{};
|
||||
for (final RegExpMatch match in keyEntry.allMatches(keyboardLayout)) {
|
||||
@@ -111,18 +116,23 @@ class PhysicalKeyData {
|
||||
// Skip undefined scan codes.
|
||||
continue;
|
||||
}
|
||||
androidNameToScanCodes.putIfAbsent(name, () => <int>[])
|
||||
.add(int.parse(match.namedGroup('id')!));
|
||||
androidNameToScanCodes
|
||||
.putIfAbsent(name, () => <int>[])
|
||||
.add(int.parse(match.namedGroup('id')!));
|
||||
}
|
||||
|
||||
// Cast Android dom map
|
||||
final Map<String, List<String>> nameToAndroidNames = (json.decode(nameMap) as Map<String, dynamic>)
|
||||
.cast<String, List<dynamic>>()
|
||||
.map<String, List<String>>((String key, List<dynamic> value) {
|
||||
return MapEntry<String, List<String>>(key, value.cast<String>());
|
||||
});
|
||||
final Map<String, List<String>> nameToAndroidNames = (json.decode(nameMap)
|
||||
as Map<String, dynamic>)
|
||||
.cast<String, List<dynamic>>()
|
||||
.map<String, List<String>>((String key, List<dynamic> value) {
|
||||
return MapEntry<String, List<String>>(key, value.cast<String>());
|
||||
});
|
||||
|
||||
final Map<String, List<int>> result = nameToAndroidNames.map((String name, List<String> androidNames) {
|
||||
final Map<String, List<int>> result = nameToAndroidNames.map((
|
||||
String name,
|
||||
List<String> androidNames,
|
||||
) {
|
||||
final Set<int> scanCodes = <int>{};
|
||||
for (final String androidName in androidNames) {
|
||||
scanCodes.addAll(androidNameToScanCodes[androidName] ?? <int>[]);
|
||||
@@ -179,14 +189,16 @@ class PhysicalKeyData {
|
||||
if (existing != null && existing.name != 'Fn') {
|
||||
// If it's an existing entry, the only thing we currently support is
|
||||
// to insert an extra DOMKey. The other entries must be empty.
|
||||
assert(evdevCode == 0
|
||||
&& xKbScanCode == 0
|
||||
&& windowsScanCode == 0
|
||||
&& macScanCode == 0xffff
|
||||
&& chromiumCode != null
|
||||
&& chromiumCode.isNotEmpty,
|
||||
'Duplicate usbHidCode ${existing.usbHidCode} of key ${existing.name} '
|
||||
'conflicts with existing ${entries[existing.usbHidCode]!.name}.');
|
||||
assert(
|
||||
evdevCode == 0 &&
|
||||
xKbScanCode == 0 &&
|
||||
windowsScanCode == 0 &&
|
||||
macScanCode == 0xffff &&
|
||||
chromiumCode != null &&
|
||||
chromiumCode.isNotEmpty,
|
||||
'Duplicate usbHidCode ${existing.usbHidCode} of key ${existing.name} '
|
||||
'conflicts with existing ${entries[existing.usbHidCode]!.name}.',
|
||||
);
|
||||
existing.otherWebCodes.add(chromiumCode!);
|
||||
continue;
|
||||
}
|
||||
@@ -203,8 +215,9 @@ class PhysicalKeyData {
|
||||
);
|
||||
entries[newEntry.usbHidCode] = newEntry;
|
||||
}
|
||||
return entries.map((int code, PhysicalKeyEntry entry) =>
|
||||
MapEntry<String, PhysicalKeyEntry>(entry.name, entry));
|
||||
return entries.map(
|
||||
(int code, PhysicalKeyEntry entry) => MapEntry<String, PhysicalKeyEntry>(entry.name, entry),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,24 +263,32 @@ class PhysicalKeyEntry {
|
||||
|
||||
/// The Evdev scan code of the key, from Chromium's header file.
|
||||
final int? evdevCode;
|
||||
|
||||
/// The XKb scan code of the key from Chromium's header file.
|
||||
final int? xKbScanCode;
|
||||
|
||||
/// The Windows scan code of the key from Chromium's header file.
|
||||
final int? windowsScanCode;
|
||||
|
||||
/// The macOS scan code of the key from Chromium's header file.
|
||||
final int? macOSScanCode;
|
||||
|
||||
/// The iOS scan code of the key from UIKey's documentation (USB Hid table)
|
||||
final int? iOSScanCode;
|
||||
|
||||
/// The list of Android scan codes matching this key, created by looking up
|
||||
/// the Android name in the Chromium data, and substituting the Android scan
|
||||
/// code value.
|
||||
final List<int> androidScanCodes;
|
||||
|
||||
/// The name of the key, mostly derived from the DomKey name in Chromium,
|
||||
/// but where there was no DomKey representation, derived from the Chromium
|
||||
/// symbol name.
|
||||
final String name;
|
||||
|
||||
/// The Chromium event code for the key.
|
||||
final String? chromiumCode;
|
||||
|
||||
/// Other codes used by Web besides chromiumCode.
|
||||
final List<String> otherWebCodes;
|
||||
|
||||
@@ -281,10 +302,7 @@ class PhysicalKeyEntry {
|
||||
/// Creates a JSON map from the key data.
|
||||
Map<String, dynamic> toJson() {
|
||||
return removeEmptyValues(<String, dynamic>{
|
||||
'names': <String, dynamic>{
|
||||
'name': name,
|
||||
'chromium': chromiumCode,
|
||||
},
|
||||
'names': <String, dynamic>{'name': name, 'chromium': chromiumCode},
|
||||
'otherWebCodes': otherWebCodes,
|
||||
'scanCodes': <String, dynamic>{
|
||||
'android': androidScanCodes,
|
||||
@@ -304,7 +322,9 @@ class PhysicalKeyEntry {
|
||||
RegExp(r'(Digit|Numpad|Lang|Button|Left|Right)([0-9]+)'),
|
||||
(Match match) => '${match.group(1)} ${match.group(2)}',
|
||||
);
|
||||
return upperCamel.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => ' ${match.group(1)}').trim();
|
||||
return upperCamel
|
||||
.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => ' ${match.group(1)}')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/// Gets the name of the key suitable for placing in comments.
|
||||
@@ -320,26 +340,26 @@ class PhysicalKeyEntry {
|
||||
/// the name from the various different names available, making sure that the
|
||||
/// name isn't a Dart reserved word (if it is, then it adds the word "Key" to
|
||||
/// the end of the name).
|
||||
late final String constantName = (() {
|
||||
String? result;
|
||||
if (name.isEmpty) {
|
||||
// If it doesn't have a DomKey name then use the Chromium symbol name.
|
||||
result = chromiumCode;
|
||||
} else {
|
||||
result = upperCamelToLowerCamel(name);
|
||||
}
|
||||
result ??= 'Key${toHex(usbHidCode)}';
|
||||
if (kDartReservedWords.contains(result)) {
|
||||
return '${result}Key';
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
late final String constantName =
|
||||
(() {
|
||||
String? result;
|
||||
if (name.isEmpty) {
|
||||
// If it doesn't have a DomKey name then use the Chromium symbol name.
|
||||
result = chromiumCode;
|
||||
} else {
|
||||
result = upperCamelToLowerCamel(name);
|
||||
}
|
||||
result ??= 'Key${toHex(usbHidCode)}';
|
||||
if (kDartReservedWords.contains(result)) {
|
||||
return '${result}Key';
|
||||
}
|
||||
return result;
|
||||
})();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String otherWebStr = otherWebCodes.isEmpty
|
||||
? ''
|
||||
: ', otherWebCodes: [${otherWebCodes.join(', ')}]';
|
||||
final String otherWebStr =
|
||||
otherWebCodes.isEmpty ? '' : ', otherWebCodes: [${otherWebCodes.join(', ')}]';
|
||||
return """'$constantName': (name: "$name", usbHidCode: ${toHex(usbHidCode)}, """
|
||||
'linuxScanCode: ${toHex(evdevCode)}, xKbScanCode: ${toHex(xKbScanCode)}, '
|
||||
'windowsKeyCode: ${toHex(windowsScanCode)}, macOSScanCode: ${toHex(macOSScanCode)}, '
|
||||
|
||||
@@ -30,10 +30,16 @@ constexpr uint64_t kPhysical${_toUpperCamel(entry.constantName)} = ${toHex(entry
|
||||
|
||||
/// Gets the generated definitions of PhysicalKeyboardKeys.
|
||||
String get _logicalDefinitions {
|
||||
final OutputLines<int> lines = OutputLines<int>('Logical Key list', behavior: DeduplicateBehavior.kSkip);
|
||||
final OutputLines<int> lines = OutputLines<int>(
|
||||
'Logical Key list',
|
||||
behavior: DeduplicateBehavior.kSkip,
|
||||
);
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
lines.add(entry.value, '''
|
||||
constexpr uint64_t kLogical${_toUpperCamel(entry.constantName)} = ${toHex(entry.value, digits: 11)};''');
|
||||
lines.add(
|
||||
entry.value,
|
||||
'''
|
||||
constexpr uint64_t kLogical${_toUpperCamel(entry.constantName)} = ${toHex(entry.value, digits: 11)};''',
|
||||
);
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
@@ -11,18 +11,16 @@ import 'utils.dart';
|
||||
|
||||
String _toUpperSnake(String lowerCamel) {
|
||||
// Converts 'myTVFoo' to 'myTvFoo'.
|
||||
final String trueUpperCamel = lowerCamel.replaceAllMapped(
|
||||
RegExp(r'([A-Z]{3,})'),
|
||||
(Match match) {
|
||||
final String matched = match.group(1)!;
|
||||
return matched.substring(0, 1)
|
||||
+ matched.substring(1, matched.length - 2).toLowerCase()
|
||||
+ matched.substring(matched.length - 2, matched.length - 1);
|
||||
});
|
||||
final String trueUpperCamel = lowerCamel.replaceAllMapped(RegExp(r'([A-Z]{3,})'), (Match match) {
|
||||
final String matched = match.group(1)!;
|
||||
return matched.substring(0, 1) +
|
||||
matched.substring(1, matched.length - 2).toLowerCase() +
|
||||
matched.substring(matched.length - 2, matched.length - 1);
|
||||
});
|
||||
// Converts 'myTvFoo' to 'MY_TV_FOO'.
|
||||
return trueUpperCamel.replaceAllMapped(
|
||||
RegExp(r'([A-Z])'),
|
||||
(Match match) => '_${match.group(1)!}').toUpperCase();
|
||||
return trueUpperCamel
|
||||
.replaceAllMapped(RegExp(r'([A-Z])'), (Match match) => '_${match.group(1)!}')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/// Generates the common/testing/key_codes.h based on the information in the key
|
||||
@@ -34,18 +32,27 @@ class KeyCodesJavaGenerator extends BaseCodeGenerator {
|
||||
String get _physicalDefinitions {
|
||||
final OutputLines<int> lines = OutputLines<int>('Physical Key list');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
lines.add(entry.usbHidCode, '''
|
||||
public static final long PHYSICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.usbHidCode)}L;''');
|
||||
lines.add(
|
||||
entry.usbHidCode,
|
||||
'''
|
||||
public static final long PHYSICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.usbHidCode)}L;''',
|
||||
);
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
/// Gets the generated definitions of PhysicalKeyboardKeys.
|
||||
String get _logicalDefinitions {
|
||||
final OutputLines<int> lines = OutputLines<int>('Logical Key list', behavior: DeduplicateBehavior.kSkip);
|
||||
final OutputLines<int> lines = OutputLines<int>(
|
||||
'Logical Key list',
|
||||
behavior: DeduplicateBehavior.kSkip,
|
||||
);
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
lines.add(entry.value, '''
|
||||
public static final long LOGICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.value, digits: 11)}L;''');
|
||||
lines.add(
|
||||
entry.value,
|
||||
'''
|
||||
public static final long LOGICAL_${_toUpperSnake(entry.constantName)} = ${toHex(entry.value, digits: 11)}L;''',
|
||||
);
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import 'constants.dart';
|
||||
|
||||
/// The location of the Flutter root directory, based on the known location of
|
||||
/// this script.
|
||||
final Directory flutterRoot = Directory(path.dirname(Platform.script.toFilePath())).parent.parent.parent.parent;
|
||||
final Directory flutterRoot =
|
||||
Directory(path.dirname(Platform.script.toFilePath())).parent.parent.parent.parent;
|
||||
String get dataRoot => testDataRoot ?? _dataRoot;
|
||||
String _dataRoot = path.join(flutterRoot.path, 'dev', 'tools', 'gen_keycodes', 'data');
|
||||
|
||||
@@ -169,14 +170,16 @@ Map<String, String> parseMapOfString(String jsonString) {
|
||||
|
||||
/// Read a Map<String, List<String>> out of its string representation in JSON.
|
||||
Map<String, List<String>> parseMapOfListOfString(String jsonString) {
|
||||
final Map<String, List<dynamic>> dynamicMap = (json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
|
||||
final Map<String, List<dynamic>> dynamicMap =
|
||||
(json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
|
||||
return dynamicMap.map<String, List<String>>((String key, List<dynamic> value) {
|
||||
return MapEntry<String, List<String>>(key, value.cast<String>());
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, List<String?>> parseMapOfListOfNullableString(String jsonString) {
|
||||
final Map<String, List<dynamic>> dynamicMap = (json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
|
||||
final Map<String, List<dynamic>> dynamicMap =
|
||||
(json.decode(jsonString) as Map<String, dynamic>).cast<String, List<dynamic>>();
|
||||
return dynamicMap.map<String, List<String?>>((String key, List<dynamic> value) {
|
||||
return MapEntry<String, List<String?>>(key, value.cast<String?>());
|
||||
});
|
||||
@@ -187,7 +190,10 @@ Map<String, bool> parseMapOfBool(String jsonString) {
|
||||
}
|
||||
|
||||
/// Reverse the map of { fromValue -> list of toValue } to { toValue -> fromValue } and return.
|
||||
Map<String, String> reverseMapOfListOfString(Map<String, List<String>> inMap, void Function(String fromValue, String newToValue) onDuplicate) {
|
||||
Map<String, String> reverseMapOfListOfString(
|
||||
Map<String, List<String>> inMap,
|
||||
void Function(String fromValue, String newToValue) onDuplicate,
|
||||
) {
|
||||
final Map<String, String> result = <String, String>{};
|
||||
inMap.forEach((String fromValue, List<String> toValues) {
|
||||
for (final String toValue in toValues) {
|
||||
@@ -205,12 +211,14 @@ Map<String, String> reverseMapOfListOfString(Map<String, List<String>> inMap, vo
|
||||
///
|
||||
/// Will modify the input map.
|
||||
Map<String, dynamic> removeEmptyValues(Map<String, dynamic> map) {
|
||||
return map..removeWhere((String key, dynamic value) => switch (value) {
|
||||
null => true,
|
||||
Map<String, dynamic>() => removeEmptyValues(value).isEmpty,
|
||||
Iterable<dynamic>() => value.isEmpty,
|
||||
_ => false,
|
||||
});
|
||||
return map..removeWhere(
|
||||
(String key, dynamic value) => switch (value) {
|
||||
null => true,
|
||||
Map<String, dynamic>() => removeEmptyValues(value).isEmpty,
|
||||
Iterable<dynamic>() => value.isEmpty,
|
||||
_ => false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void addNameValue(List<String> names, List<int> values, String name, int value) {
|
||||
@@ -238,8 +246,7 @@ enum DeduplicateBehavior {
|
||||
|
||||
/// The information for a line used by [OutputLines].
|
||||
class OutputLine<T extends Comparable<Object>> {
|
||||
OutputLine(this.key, String value)
|
||||
: values = <String>[value];
|
||||
OutputLine(this.key, String value) : values = <String>[value];
|
||||
|
||||
final T key;
|
||||
final List<String> values;
|
||||
@@ -268,7 +275,9 @@ class OutputLines<T extends Comparable<Object>> {
|
||||
if (existing != null) {
|
||||
switch (behavior) {
|
||||
case DeduplicateBehavior.kWarn:
|
||||
print('Warn: Request to add $key to map "$mapName" as:\n $line\n but it already exists as:\n ${existing.values[0]}');
|
||||
print(
|
||||
'Warn: Request to add $key to map "$mapName" as:\n $line\n but it already exists as:\n ${existing.values[0]}',
|
||||
);
|
||||
return;
|
||||
case DeduplicateBehavior.kSkip:
|
||||
return;
|
||||
@@ -286,9 +295,9 @@ class OutputLines<T extends Comparable<Object>> {
|
||||
|
||||
String sortedJoin() {
|
||||
return (lines.values.toList()
|
||||
..sort((OutputLine<T> a, OutputLine<T> b) => a.key.compareTo(b.key)))
|
||||
.map((OutputLine<T> line) => line.values.join('\n'))
|
||||
.join('\n');
|
||||
..sort((OutputLine<T> a, OutputLine<T> b) => a.key.compareTo(b.key)))
|
||||
.map((OutputLine<T> line) => line.values.join('\n'))
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,8 @@ import 'utils.dart';
|
||||
/// Generates the key mapping for Web, based on the information in the key
|
||||
/// data structure given to it.
|
||||
class WebCodeGenerator extends PlatformCodeGenerator {
|
||||
WebCodeGenerator(
|
||||
super.keyData,
|
||||
super.logicalData,
|
||||
String logicalLocationMap,
|
||||
) : _logicalLocationMap = parseMapOfListOfNullableString(logicalLocationMap);
|
||||
WebCodeGenerator(super.keyData, super.logicalData, String logicalLocationMap)
|
||||
: _logicalLocationMap = parseMapOfListOfNullableString(logicalLocationMap);
|
||||
|
||||
/// This generates the map of Web KeyboardEvent codes to logical key ids.
|
||||
String get _webLogicalKeyCodeMap {
|
||||
@@ -38,8 +35,7 @@ class WebCodeGenerator extends PlatformCodeGenerator {
|
||||
final OutputLines<String> lines = OutputLines<String>('Web physical map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
for (final String webCode in entry.webCodes()) {
|
||||
lines.add(webCode,
|
||||
" '$webCode': ${toHex(entry.usbHidCode)}, // ${entry.constantName}");
|
||||
lines.add(webCode, " '$webCode': ${toHex(entry.usbHidCode)}, // ${entry.constantName}");
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -49,24 +45,36 @@ class WebCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _webLogicalLocationMap {
|
||||
final OutputLines<String> lines = OutputLines<String>('Web logical location map');
|
||||
_logicalLocationMap.forEach((String webKey, List<String?> locations) {
|
||||
final String valuesString = locations.map((String? value) {
|
||||
return value == null ? 'null' : toHex(logicalData.entryByName(value).value, digits: 11);
|
||||
}).join(', ');
|
||||
final String namesString = locations.map((String? value) {
|
||||
return value == null ? 'null' : logicalData.entryByName(value).constantName;
|
||||
}).join(', ');
|
||||
final String valuesString = locations
|
||||
.map((String? value) {
|
||||
return value == null ? 'null' : toHex(logicalData.entryByName(value).value, digits: 11);
|
||||
})
|
||||
.join(', ');
|
||||
final String namesString = locations
|
||||
.map((String? value) {
|
||||
return value == null ? 'null' : logicalData.entryByName(value).constantName;
|
||||
})
|
||||
.join(', ');
|
||||
lines.add(webKey, " '$webKey': <int?>[$valuesString], // $namesString");
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
final Map<String, List<String?>> _logicalLocationMap;
|
||||
|
||||
@override
|
||||
String get templatePath => path.join(dataRoot, 'web_key_map_dart.tmpl');
|
||||
|
||||
@override
|
||||
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
|
||||
'lib', 'web_ui', 'lib', 'src', 'engine', 'key_map.g.dart');
|
||||
String outputPath(String platform) => path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'lib',
|
||||
'web_ui',
|
||||
'lib',
|
||||
'src',
|
||||
'engine',
|
||||
'key_map.g.dart',
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, String> mappings() {
|
||||
|
||||
@@ -13,19 +13,18 @@ import 'utils.dart';
|
||||
/// Generates the key mapping for Windows, based on the information in the key
|
||||
/// data structure given to it.
|
||||
class WindowsCodeGenerator extends PlatformCodeGenerator {
|
||||
WindowsCodeGenerator(
|
||||
super.keyData,
|
||||
super.logicalData,
|
||||
String scancodeToLogical,
|
||||
) : _scancodeToLogical = parseMapOfString(scancodeToLogical);
|
||||
WindowsCodeGenerator(super.keyData, super.logicalData, String scancodeToLogical)
|
||||
: _scancodeToLogical = parseMapOfString(scancodeToLogical);
|
||||
|
||||
/// This generates the map of Windows scan codes to physical keys.
|
||||
String get _windowsScanCodeMap {
|
||||
final OutputLines<int> lines = OutputLines<int>('Windows scancode map');
|
||||
for (final PhysicalKeyEntry entry in keyData.entries) {
|
||||
if (entry.windowsScanCode != null) {
|
||||
lines.add(entry.windowsScanCode!,
|
||||
' {${toHex(entry.windowsScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}');
|
||||
lines.add(
|
||||
entry.windowsScanCode!,
|
||||
' {${toHex(entry.windowsScanCode)}, ${toHex(entry.usbHidCode)}}, // ${entry.constantName}',
|
||||
);
|
||||
}
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
@@ -35,13 +34,13 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
|
||||
String get _windowsLogicalKeyCodeMap {
|
||||
final OutputLines<int> lines = OutputLines<int>('Windows logical map');
|
||||
for (final LogicalKeyEntry entry in logicalData.entries) {
|
||||
zipStrict(entry.windowsValues, entry.windowsNames,
|
||||
(int windowsValue, String windowsName) {
|
||||
lines.add(windowsValue,
|
||||
' {${toHex(windowsValue)}, ${toHex(entry.value, digits: 11)}}, '
|
||||
'// $windowsName -> ${entry.constantName}');
|
||||
},
|
||||
);
|
||||
zipStrict(entry.windowsValues, entry.windowsNames, (int windowsValue, String windowsName) {
|
||||
lines.add(
|
||||
windowsValue,
|
||||
' {${toHex(windowsValue)}, ${toHex(entry.value, digits: 11)}}, '
|
||||
'// $windowsName -> ${entry.constantName}',
|
||||
);
|
||||
});
|
||||
}
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
@@ -56,12 +55,15 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
|
||||
_scancodeToLogical.forEach((String scanCodeName, String logicalName) {
|
||||
final PhysicalKeyEntry physicalEntry = keyData.entryByName(scanCodeName);
|
||||
final LogicalKeyEntry logicalEntry = logicalData.entryByName(logicalName);
|
||||
lines.add(physicalEntry.windowsScanCode!,
|
||||
' {${toHex(physicalEntry.windowsScanCode)}, ${toHex(logicalEntry.value, digits: 11)}}, '
|
||||
'// ${physicalEntry.constantName} -> ${logicalEntry.constantName}');
|
||||
lines.add(
|
||||
physicalEntry.windowsScanCode!,
|
||||
' {${toHex(physicalEntry.windowsScanCode)}, ${toHex(logicalEntry.value, digits: 11)}}, '
|
||||
'// ${physicalEntry.constantName} -> ${logicalEntry.constantName}',
|
||||
);
|
||||
});
|
||||
return lines.sortedJoin().trimRight();
|
||||
}
|
||||
|
||||
final Map<String, String> _scancodeToLogical;
|
||||
|
||||
/// This generates the mask values for the part of a key code that defines its plane.
|
||||
@@ -73,7 +75,9 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
|
||||
kWindowsPlane,
|
||||
];
|
||||
for (final MaskConstant constant in maskConstants) {
|
||||
buffer.writeln('const uint64_t KeyboardKeyEmbedderHandler::${constant.lowerCamelName} = ${toHex(constant.value, digits: 11)};');
|
||||
buffer.writeln(
|
||||
'const uint64_t KeyboardKeyEmbedderHandler::${constant.lowerCamelName} = ${toHex(constant.value, digits: 11)};',
|
||||
);
|
||||
}
|
||||
return buffer.toString().trimRight();
|
||||
}
|
||||
@@ -82,8 +86,13 @@ class WindowsCodeGenerator extends PlatformCodeGenerator {
|
||||
String get templatePath => path.join(dataRoot, 'windows_flutter_key_map_cc.tmpl');
|
||||
|
||||
@override
|
||||
String outputPath(String platform) => path.join(PlatformCodeGenerator.engineRoot,
|
||||
'shell', 'platform', 'windows', 'flutter_key_map.g.cc');
|
||||
String outputPath(String platform) => path.join(
|
||||
PlatformCodeGenerator.engineRoot,
|
||||
'shell',
|
||||
'platform',
|
||||
'windows',
|
||||
'flutter_key_map.g.cc',
|
||||
);
|
||||
|
||||
@override
|
||||
Map<String, String> mappings() {
|
||||
|
||||
@@ -23,18 +23,19 @@ String readDataFile(String fileName) {
|
||||
}
|
||||
|
||||
final PhysicalKeyData physicalData = PhysicalKeyData.fromJson(
|
||||
json.decode(readDataFile('physical_key_data.g.json')) as Map<String, dynamic>);
|
||||
json.decode(readDataFile('physical_key_data.g.json')) as Map<String, dynamic>,
|
||||
);
|
||||
final LogicalKeyData logicalData = LogicalKeyData.fromJson(
|
||||
json.decode(readDataFile('logical_key_data.g.json')) as Map<String, dynamic>);
|
||||
final Map<String, bool> keyGoals = parseMapOfBool(
|
||||
readDataFile('layout_goals.json'));
|
||||
json.decode(readDataFile('logical_key_data.g.json')) as Map<String, dynamic>,
|
||||
);
|
||||
final Map<String, bool> keyGoals = parseMapOfBool(readDataFile('layout_goals.json'));
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
testDataRoot = path.canonicalize(path.join(Directory.current.absolute.path, 'data'));
|
||||
});
|
||||
|
||||
tearDown((){
|
||||
tearDown(() {
|
||||
testDataRoot = null;
|
||||
});
|
||||
|
||||
@@ -50,10 +51,7 @@ void main() {
|
||||
|
||||
test('Generate Keycodes for Android', () {
|
||||
const String platform = 'android';
|
||||
final PlatformCodeGenerator codeGenerator = AndroidCodeGenerator(
|
||||
physicalData,
|
||||
logicalData,
|
||||
);
|
||||
final PlatformCodeGenerator codeGenerator = AndroidCodeGenerator(physicalData, logicalData);
|
||||
final String output = codeGenerator.generate();
|
||||
|
||||
expect(codeGenerator.outputPath(platform), endsWith('KeyboardMap.java'));
|
||||
@@ -84,10 +82,7 @@ void main() {
|
||||
});
|
||||
test('Generate Keycodes for iOS', () {
|
||||
const String platform = 'ios';
|
||||
final PlatformCodeGenerator codeGenerator = IOSCodeGenerator(
|
||||
physicalData,
|
||||
logicalData,
|
||||
);
|
||||
final PlatformCodeGenerator codeGenerator = IOSCodeGenerator(physicalData, logicalData);
|
||||
final String output = codeGenerator.generate();
|
||||
|
||||
expect(codeGenerator.outputPath(platform), endsWith('KeyCodeMap.g.mm'));
|
||||
@@ -152,20 +147,12 @@ void main() {
|
||||
|
||||
// Regression tests for https://github.com/flutter/flutter/pull/87098
|
||||
|
||||
expect(
|
||||
entries.indexWhere((LogicalKeyEntry entry) => entry.name == 'ShiftLeft'),
|
||||
isNot(-1));
|
||||
expect(
|
||||
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('ShiftLeft')),
|
||||
-1);
|
||||
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.name == 'ShiftLeft'), isNot(-1));
|
||||
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('ShiftLeft')), -1);
|
||||
// 'Shift' maps to both 'ShiftLeft' and 'ShiftRight', and should be resolved
|
||||
// by other ways.
|
||||
expect(
|
||||
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Shift')),
|
||||
-1);
|
||||
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Shift')), -1);
|
||||
// Printable keys must not be added with Web key of their names.
|
||||
expect(
|
||||
entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Slash')),
|
||||
-1);
|
||||
expect(entries.indexWhere((LogicalKeyEntry entry) => entry.webNames.contains('Slash')), -1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,10 +37,11 @@ void _encodeBundleTranslations(Map<String, dynamic> bundle) {
|
||||
final String translation = bundle[key] as String;
|
||||
// Rewrite the string as a series of unicode characters in JSON format.
|
||||
// Like "\u0012\u0123\u1234".
|
||||
bundle[key] = translation.runes.map((int code) {
|
||||
final String codeString = '00${code.toRadixString(16)}';
|
||||
return '\\u${codeString.substring(codeString.length - 4)}';
|
||||
}).join();
|
||||
bundle[key] =
|
||||
translation.runes.map((int code) {
|
||||
final String codeString = '00${code.toRadixString(16)}';
|
||||
return '\\u${codeString.substring(codeString.length - 4)}';
|
||||
}).join();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +50,9 @@ void _checkEncodedTranslations(Map<String, dynamic> encodedBundle, Map<String, d
|
||||
const JsonDecoder decoder = JsonDecoder();
|
||||
for (final String key in bundle.keys) {
|
||||
if (decoder.convert('"${encodedBundle[key]}"') != bundle[key]) {
|
||||
stderr.writeln(' encodedTranslation for $key does not match original value "${bundle[key]}"');
|
||||
stderr.writeln(
|
||||
' encodedTranslation for $key does not match original value "${bundle[key]}"',
|
||||
);
|
||||
errorFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,19 +46,21 @@ Future<void> main(List<String> rawArgs) async {
|
||||
|
||||
final bool writeToFile = parseArgs(rawArgs).writeToFile;
|
||||
|
||||
final File packageConfigFile = File(path.join('packages', 'flutter_localizations', '.dart_tool', 'package_config.json'));
|
||||
final File packageConfigFile = File(
|
||||
path.join('packages', 'flutter_localizations', '.dart_tool', 'package_config.json'),
|
||||
);
|
||||
final bool packageConfigExists = packageConfigFile.existsSync();
|
||||
|
||||
if (!packageConfigExists) {
|
||||
exitWithError(
|
||||
'File not found: ${packageConfigFile.path}. $_kCommandName must be run '
|
||||
'after a successful "flutter update-packages".'
|
||||
'after a successful "flutter update-packages".',
|
||||
);
|
||||
}
|
||||
|
||||
final List<Object?> packages = (
|
||||
json.decode(packageConfigFile.readAsStringSync()) as Map<String, Object?>
|
||||
)['packages']! as List<Object?>;
|
||||
final List<Object?> packages =
|
||||
(json.decode(packageConfigFile.readAsStringSync()) as Map<String, Object?>)['packages']!
|
||||
as List<Object?>;
|
||||
|
||||
String? pathToIntl;
|
||||
for (final Object? package in packages) {
|
||||
@@ -72,19 +74,22 @@ Future<void> main(List<String> rawArgs) async {
|
||||
if (pathToIntl == null) {
|
||||
exitWithError(
|
||||
'Could not find "intl" package. $_kCommandName must be run '
|
||||
'after a successful "flutter update-packages".'
|
||||
'after a successful "flutter update-packages".',
|
||||
);
|
||||
}
|
||||
|
||||
final Directory dateSymbolsDirectory = Directory(path.join(pathToIntl!, 'lib', 'src', 'data', 'dates', 'symbols'));
|
||||
final Directory dateSymbolsDirectory = Directory(
|
||||
path.join(pathToIntl!, 'lib', 'src', 'data', 'dates', 'symbols'),
|
||||
);
|
||||
final Map<String, File> symbolFiles = _listIntlData(dateSymbolsDirectory);
|
||||
final Directory datePatternsDirectory = Directory(path.join(pathToIntl, 'lib', 'src', 'data', 'dates', 'patterns'));
|
||||
final Directory datePatternsDirectory = Directory(
|
||||
path.join(pathToIntl, 'lib', 'src', 'data', 'dates', 'patterns'),
|
||||
);
|
||||
final Map<String, File> patternFiles = _listIntlData(datePatternsDirectory);
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
final Set<String> supportedLocales = _supportedLocales();
|
||||
|
||||
buffer.writeln(
|
||||
'''
|
||||
buffer.writeln('''
|
||||
// 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.
|
||||
@@ -95,8 +100,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
|
||||
import 'package:intl/date_symbols.dart' as intl;
|
||||
|
||||
'''
|
||||
);
|
||||
''');
|
||||
buffer.writeln('''
|
||||
/// The subset of date symbols supported by the intl package which are also
|
||||
/// supported by flutter_localizations.''');
|
||||
@@ -104,7 +108,8 @@ import 'package:intl/date_symbols.dart' as intl;
|
||||
symbolFiles.forEach((String locale, File data) {
|
||||
currentLocale = locale;
|
||||
if (supportedLocales.contains(locale)) {
|
||||
final Map<String, Object?> objData = json.decode(data.readAsStringSync()) as Map<String, Object?>;
|
||||
final Map<String, Object?> objData =
|
||||
json.decode(data.readAsStringSync()) as Map<String, Object?>;
|
||||
buffer.writeln("'$locale': intl.DateSymbols(");
|
||||
objData.forEach((String key, Object? value) {
|
||||
if (value == null) {
|
||||
@@ -123,10 +128,13 @@ import 'package:intl/date_symbols.dart' as intl;
|
||||
buffer.writeln('''
|
||||
/// The subset of date patterns supported by the intl package which are also
|
||||
/// supported by flutter_localizations.''');
|
||||
buffer.writeln('const Map<String, Map<String, String>> datePatterns = <String, Map<String, String>> {');
|
||||
buffer.writeln(
|
||||
'const Map<String, Map<String, String>> datePatterns = <String, Map<String, String>> {',
|
||||
);
|
||||
patternFiles.forEach((String locale, File data) {
|
||||
if (supportedLocales.contains(locale)) {
|
||||
final Map<String, dynamic> patterns = json.decode(data.readAsStringSync()) as Map<String, dynamic>;
|
||||
final Map<String, dynamic> patterns =
|
||||
json.decode(data.readAsStringSync()) as Map<String, dynamic>;
|
||||
buffer.writeln("'$locale': <String, String>{");
|
||||
patterns.forEach((String key, dynamic value) {
|
||||
assert(value is String);
|
||||
@@ -138,13 +146,22 @@ import 'package:intl/date_symbols.dart' as intl;
|
||||
buffer.writeln('};');
|
||||
|
||||
if (writeToFile) {
|
||||
final File dateLocalizationsFile = File(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n', 'generated_date_localizations.dart'));
|
||||
final File dateLocalizationsFile = File(
|
||||
path.join(
|
||||
'packages',
|
||||
'flutter_localizations',
|
||||
'lib',
|
||||
'src',
|
||||
'l10n',
|
||||
'generated_date_localizations.dart',
|
||||
),
|
||||
);
|
||||
dateLocalizationsFile.writeAsStringSync(buffer.toString());
|
||||
final String extension = Platform.isWindows ? '.exe' : '';
|
||||
final ProcessResult result = Process.runSync(path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart$extension'), <String>[
|
||||
'format',
|
||||
dateLocalizationsFile.path,
|
||||
]);
|
||||
final ProcessResult result = Process.runSync(
|
||||
path.join('bin', 'cache', 'dart-sdk', 'bin', 'dart$extension'),
|
||||
<String>['format', dateLocalizationsFile.path],
|
||||
);
|
||||
if (result.exitCode != 0) {
|
||||
print(result.exitCode);
|
||||
print(result.stdout);
|
||||
@@ -220,11 +237,11 @@ Set<String> _supportedLocales() {
|
||||
// date patterns and symbols may cause problems.
|
||||
//
|
||||
// For more context, see https://github.com/flutter/flutter/issues/67644.
|
||||
final Set<String> supportedLocales = <String>{
|
||||
'en_US',
|
||||
};
|
||||
final Set<String> supportedLocales = <String>{'en_US'};
|
||||
final RegExp filenameRE = RegExp(r'(?:material|cupertino)_(\w+)\.arb$');
|
||||
final Directory supportedLocalesDirectory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
|
||||
final Directory supportedLocalesDirectory = Directory(
|
||||
path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'),
|
||||
);
|
||||
for (final FileSystemEntity entity in supportedLocalesDirectory.listSync()) {
|
||||
final String filePath = entity.path;
|
||||
if (FileSystemEntity.isFileSync(filePath) && filenameRE.hasMatch(filePath)) {
|
||||
@@ -237,10 +254,9 @@ Set<String> _supportedLocales() {
|
||||
|
||||
Map<String, File> _listIntlData(Directory directory) {
|
||||
final Map<String, File> localeFiles = <String, File>{};
|
||||
final Iterable<File> files = directory
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((File file) => file.path.endsWith('.json'));
|
||||
final Iterable<File> files = directory.listSync().whereType<File>().where(
|
||||
(File file) => file.path.endsWith('.json'),
|
||||
);
|
||||
for (final File file in files) {
|
||||
final String locale = path.basenameWithoutExtension(file.path);
|
||||
localeFiles[locale] = file;
|
||||
|
||||
@@ -83,7 +83,9 @@ String generateArbBasedLocalizationSubclasses({
|
||||
assert(supportedLanguagesDocMacro.isNotEmpty);
|
||||
generateConstructorForCountrySubClass ??= generateConstructor;
|
||||
final StringBuffer output = StringBuffer();
|
||||
output.writeln(generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'));
|
||||
output.writeln(
|
||||
generateHeader('dart dev/tools/localization/bin/gen_localizations.dart --overwrite'),
|
||||
);
|
||||
|
||||
final StringBuffer supportedLocales = StringBuffer();
|
||||
|
||||
@@ -141,7 +143,8 @@ String generateArbBasedLocalizationSubclasses({
|
||||
|
||||
final Map<String, String> languageResources = localeToResources[languageLocale]!;
|
||||
for (final String key in allKeys) {
|
||||
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
final Map<String, dynamic>? attributes =
|
||||
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
output.writeln(generateGetter(key, languageResources[key], attributes, languageLocale));
|
||||
}
|
||||
output.writeln('}');
|
||||
@@ -153,18 +156,21 @@ String generateArbBasedLocalizationSubclasses({
|
||||
// script default values before language default values.
|
||||
for (final String scriptCode in languageToScriptCodes[languageName]!) {
|
||||
final LocaleInfo scriptBaseLocale = LocaleInfo.fromString('${languageName}_$scriptCode');
|
||||
output.writeln(generateClassDeclaration(
|
||||
scriptBaseLocale,
|
||||
generatedClassPrefix,
|
||||
'$generatedClassPrefix${languageLocale.camelCase()}',
|
||||
));
|
||||
output.writeln(
|
||||
generateClassDeclaration(
|
||||
scriptBaseLocale,
|
||||
generatedClassPrefix,
|
||||
'$generatedClassPrefix${languageLocale.camelCase()}',
|
||||
),
|
||||
);
|
||||
output.writeln(generateConstructorForCountrySubClass(scriptBaseLocale));
|
||||
final Map<String, String> scriptResources = localeToResources[scriptBaseLocale]!;
|
||||
for (final String key in scriptResources.keys.toList()..sort()) {
|
||||
if (languageResources[key] == scriptResources[key]) {
|
||||
continue;
|
||||
}
|
||||
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
final Map<String, dynamic>? attributes =
|
||||
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
output.writeln(generateGetter(key, scriptResources[key], attributes, languageLocale));
|
||||
}
|
||||
output.writeln('}');
|
||||
@@ -181,22 +187,27 @@ String generateArbBasedLocalizationSubclasses({
|
||||
continue;
|
||||
}
|
||||
countryCodeCount += 1;
|
||||
output.writeln(generateClassDeclaration(
|
||||
locale,
|
||||
generatedClassPrefix,
|
||||
'$generatedClassPrefix${scriptBaseLocale.camelCase()}',
|
||||
));
|
||||
output.writeln(
|
||||
generateClassDeclaration(
|
||||
locale,
|
||||
generatedClassPrefix,
|
||||
'$generatedClassPrefix${scriptBaseLocale.camelCase()}',
|
||||
),
|
||||
);
|
||||
output.writeln(generateConstructorForCountrySubClass(locale));
|
||||
final Map<String, String> localeResources = localeToResources[locale]!;
|
||||
for (final String key in localeResources.keys) {
|
||||
// When script fallback contains the key, we compare to it instead of language fallback.
|
||||
if (scriptResources.containsKey(key) ? scriptResources[key] == localeResources[key] : languageResources[key] == localeResources[key]) {
|
||||
if (scriptResources.containsKey(key)
|
||||
? scriptResources[key] == localeResources[key]
|
||||
: languageResources[key] == localeResources[key]) {
|
||||
continue;
|
||||
}
|
||||
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
final Map<String, dynamic>? attributes =
|
||||
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
|
||||
}
|
||||
output.writeln('}');
|
||||
output.writeln('}');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -209,35 +220,46 @@ String generateArbBasedLocalizationSubclasses({
|
||||
}
|
||||
countryCodeCount += 1;
|
||||
final Map<String, String> localeResources = localeToResources[locale]!;
|
||||
output.writeln(generateClassDeclaration(
|
||||
locale,
|
||||
generatedClassPrefix,
|
||||
'$generatedClassPrefix${languageLocale.camelCase()}',
|
||||
));
|
||||
output.writeln(
|
||||
generateClassDeclaration(
|
||||
locale,
|
||||
generatedClassPrefix,
|
||||
'$generatedClassPrefix${languageLocale.camelCase()}',
|
||||
),
|
||||
);
|
||||
output.writeln(generateConstructorForCountrySubClass(locale));
|
||||
for (final String key in localeResources.keys) {
|
||||
if (languageResources[key] == localeResources[key]) {
|
||||
continue;
|
||||
}
|
||||
final Map<String, dynamic>? attributes = localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
final Map<String, dynamic>? attributes =
|
||||
localeToResourceAttributes[canonicalLocale]![key] as Map<String, dynamic>?;
|
||||
output.writeln(generateGetter(key, localeResources[key], attributes, languageLocale));
|
||||
}
|
||||
output.writeln('}');
|
||||
output.writeln('}');
|
||||
}
|
||||
}
|
||||
|
||||
final String scriptCodeMessage = scriptCodeCount == 0 ? '' : ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
|
||||
final String scriptCodeMessage =
|
||||
scriptCodeCount == 0
|
||||
? ''
|
||||
: ' and $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'}';
|
||||
if (countryCodeCount == 0) {
|
||||
if (scriptCodeCount == 0) {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)}');
|
||||
} else {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})');
|
||||
supportedLocales.writeln(
|
||||
'/// * `$languageName` - ${describeLocale(languageName)} (plus $scriptCodeCount script${scriptCodeCount == 1 ? '' : 's'})',
|
||||
);
|
||||
}
|
||||
|
||||
} else if (countryCodeCount == 1) {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)');
|
||||
supportedLocales.writeln(
|
||||
'/// * `$languageName` - ${describeLocale(languageName)} (plus one country variation$scriptCodeMessage)',
|
||||
);
|
||||
} else {
|
||||
supportedLocales.writeln('/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)');
|
||||
supportedLocales.writeln(
|
||||
'/// * `$languageName` - ${describeLocale(languageName)} (plus $countryCodeCount country variations$scriptCodeMessage)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,10 +301,13 @@ $factoryDeclaration
|
||||
for (final String language in languageToLocales.keys) {
|
||||
// Only one instance of the language.
|
||||
if (languageToLocales[language]!.length == 1) {
|
||||
output.writeln('''
|
||||
output.writeln(
|
||||
'''
|
||||
case '$language':
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''');
|
||||
} else if (!languageToScriptCodes.containsKey(language)) { // Does not distinguish between scripts. Switch on countryCode directly.
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${languageToLocales[language]![0].camelCase()}($factoryArguments);''',
|
||||
);
|
||||
} else if (!languageToScriptCodes.containsKey(language)) {
|
||||
// Does not distinguish between scripts. Switch on countryCode directly.
|
||||
output.writeln('''
|
||||
case '$language': {
|
||||
switch (locale.countryCode) {''');
|
||||
@@ -292,15 +317,18 @@ $factoryDeclaration
|
||||
}
|
||||
assert(locale.length > 1);
|
||||
final String countryCode = locale.countryCode!;
|
||||
output.writeln('''
|
||||
output.writeln(
|
||||
'''
|
||||
case '$countryCode':
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
|
||||
);
|
||||
}
|
||||
output.writeln('''
|
||||
}
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
|
||||
}''');
|
||||
} else { // Language has scriptCode, add additional switch logic.
|
||||
} else {
|
||||
// Language has scriptCode, add additional switch logic.
|
||||
bool hasCountryCode = false;
|
||||
output.writeln('''
|
||||
case '$language': {
|
||||
@@ -325,9 +353,11 @@ $factoryDeclaration
|
||||
continue;
|
||||
}
|
||||
final String countryCode = locale.countryCode!;
|
||||
output.writeln('''
|
||||
output.writeln(
|
||||
'''
|
||||
case '$countryCode':
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
|
||||
);
|
||||
}
|
||||
}
|
||||
// Return a fallback locale that matches scriptCode, but not countryCode.
|
||||
@@ -339,7 +369,7 @@ $factoryDeclaration
|
||||
}''');
|
||||
}
|
||||
output.writeln('''
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
|
||||
}''');
|
||||
} else {
|
||||
// Not Explicitly defined, fallback to first locale with the same language and
|
||||
@@ -353,7 +383,7 @@ $factoryDeclaration
|
||||
}''');
|
||||
}
|
||||
output.writeln('''
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${scriptLocale.camelCase()}($factoryArguments);
|
||||
}''');
|
||||
break;
|
||||
}
|
||||
@@ -362,7 +392,7 @@ $factoryDeclaration
|
||||
output.writeln('''
|
||||
}''');
|
||||
if (hasCountryCode) {
|
||||
output.writeln('''
|
||||
output.writeln('''
|
||||
switch (locale.countryCode) {''');
|
||||
for (final LocaleInfo locale in languageToLocales[language]!) {
|
||||
if (locale.originalString == language) {
|
||||
@@ -373,15 +403,17 @@ $factoryDeclaration
|
||||
continue;
|
||||
}
|
||||
final String countryCode = locale.countryCode!;
|
||||
output.writeln('''
|
||||
output.writeln(
|
||||
'''
|
||||
case '$countryCode':
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''');
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${locale.camelCase()}($factoryArguments);''',
|
||||
);
|
||||
}
|
||||
output.writeln('''
|
||||
}''');
|
||||
}
|
||||
output.writeln('''
|
||||
return ${callsFactoryWithConst ? 'const ': ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
|
||||
return ${callsFactoryWithConst ? 'const ' : ''}$generatedClassPrefix${LocaleInfo.fromString(language).camelCase()}($factoryArguments);
|
||||
}''');
|
||||
}
|
||||
}
|
||||
@@ -472,7 +504,7 @@ String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInf
|
||||
throw Exception(
|
||||
'"$value" is not one of the ICU short time patterns supported '
|
||||
'by the material library. Here is the list of supported '
|
||||
'patterns:\n ${_icuTimeOfDayToEnum.keys.join('\n ')}'
|
||||
'patterns:\n ${_icuTimeOfDayToEnum.keys.join('\n ')}',
|
||||
);
|
||||
}
|
||||
return _icuTimeOfDayToEnum[value];
|
||||
@@ -481,23 +513,28 @@ String? generateValue(String? value, Map<String, dynamic>? attributes, LocaleInf
|
||||
throw Exception(
|
||||
'"$value" is not one of the scriptCategory values supported '
|
||||
'by the material library. Here is the list of supported '
|
||||
'values:\n ${_scriptCategoryToEnum.keys.join('\n ')}'
|
||||
'values:\n ${_scriptCategoryToEnum.keys.join('\n ')}',
|
||||
);
|
||||
}
|
||||
return _scriptCategoryToEnum[value];
|
||||
}
|
||||
}
|
||||
return generateEncodedString(locale.languageCode, value);
|
||||
return generateEncodedString(locale.languageCode, value);
|
||||
}
|
||||
|
||||
/// Combines [generateType], [generateKey], and [generateValue] to return
|
||||
/// the source of getters for the GlobalMaterialLocalizations subclass.
|
||||
/// The locale is the locale for which the getter is being generated.
|
||||
String generateGetter(String key, String? value, Map<String, dynamic>? attributes, LocaleInfo locale) {
|
||||
String generateGetter(
|
||||
String key,
|
||||
String? value,
|
||||
Map<String, dynamic>? attributes,
|
||||
LocaleInfo locale,
|
||||
) {
|
||||
final String type = generateType(attributes);
|
||||
key = generateKey(key, attributes);
|
||||
final String? generatedValue = generateValue(value, attributes, locale);
|
||||
return '''
|
||||
return '''
|
||||
|
||||
@override
|
||||
$type get $key => $generatedValue;''';
|
||||
@@ -511,7 +548,9 @@ void main(List<String> rawArgs) {
|
||||
// is the 2nd command line argument, lc is a language code and cc is the country
|
||||
// code. In most cases both codes are just two characters.
|
||||
|
||||
final Directory directory = Directory(path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'));
|
||||
final Directory directory = Directory(
|
||||
path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n'),
|
||||
);
|
||||
final RegExp widgetsFilenameRE = RegExp(r'widgets_(\w+)\.arb$');
|
||||
final RegExp materialFilenameRE = RegExp(r'material_(\w+)\.arb$');
|
||||
final RegExp cupertinoFilenameRE = RegExp(r'cupertino_(\w+)\.arb$');
|
||||
@@ -537,22 +576,28 @@ void main(List<String> rawArgs) {
|
||||
precacheLanguageAndRegionTags();
|
||||
|
||||
// Maps of locales to resource key/value pairs for Widgets ARBs.
|
||||
final Map<LocaleInfo, Map<String, String>> widgetsLocaleToResources = <LocaleInfo, Map<String, String>>{};
|
||||
final Map<LocaleInfo, Map<String, String>> widgetsLocaleToResources =
|
||||
<LocaleInfo, Map<String, String>>{};
|
||||
// Maps of locales to resource key/attributes pairs for Widgets ARBs.
|
||||
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
|
||||
final Map<LocaleInfo, Map<String, dynamic>> widgetsLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
|
||||
final Map<LocaleInfo, Map<String, dynamic>> widgetsLocaleToResourceAttributes =
|
||||
<LocaleInfo, Map<String, dynamic>>{};
|
||||
|
||||
// Maps of locales to resource key/value pairs for Material ARBs.
|
||||
final Map<LocaleInfo, Map<String, String>> materialLocaleToResources = <LocaleInfo, Map<String, String>>{};
|
||||
final Map<LocaleInfo, Map<String, String>> materialLocaleToResources =
|
||||
<LocaleInfo, Map<String, String>>{};
|
||||
// Maps of locales to resource key/attributes pairs for Material ARBs.
|
||||
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
|
||||
final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
|
||||
final Map<LocaleInfo, Map<String, dynamic>> materialLocaleToResourceAttributes =
|
||||
<LocaleInfo, Map<String, dynamic>>{};
|
||||
|
||||
// Maps of locales to resource key/value pairs for Cupertino ARBs.
|
||||
final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources = <LocaleInfo, Map<String, String>>{};
|
||||
final Map<LocaleInfo, Map<String, String>> cupertinoLocaleToResources =
|
||||
<LocaleInfo, Map<String, String>>{};
|
||||
// Maps of locales to resource key/attributes pairs for Cupertino ARBs.
|
||||
// https://github.com/googlei18n/app-resource-bundle/wiki/ApplicationResourceBundleSpecification#resource-attributes
|
||||
final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes = <LocaleInfo, Map<String, dynamic>>{};
|
||||
final Map<LocaleInfo, Map<String, dynamic>> cupertinoLocaleToResourceAttributes =
|
||||
<LocaleInfo, Map<String, dynamic>>{};
|
||||
|
||||
loadMatchingArbsIntoBundleMaps(
|
||||
directory: directory,
|
||||
@@ -574,9 +619,21 @@ void main(List<String> rawArgs) {
|
||||
);
|
||||
|
||||
try {
|
||||
validateLocalizations(widgetsLocaleToResources, widgetsLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
|
||||
validateLocalizations(materialLocaleToResources, materialLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
|
||||
validateLocalizations(cupertinoLocaleToResources, cupertinoLocaleToResourceAttributes, removeUndefined: options.removeUndefined);
|
||||
validateLocalizations(
|
||||
widgetsLocaleToResources,
|
||||
widgetsLocaleToResourceAttributes,
|
||||
removeUndefined: options.removeUndefined,
|
||||
);
|
||||
validateLocalizations(
|
||||
materialLocaleToResources,
|
||||
materialLocaleToResourceAttributes,
|
||||
removeUndefined: options.removeUndefined,
|
||||
);
|
||||
validateLocalizations(
|
||||
cupertinoLocaleToResources,
|
||||
cupertinoLocaleToResourceAttributes,
|
||||
removeUndefined: options.removeUndefined,
|
||||
);
|
||||
} on ValidationError catch (exception) {
|
||||
exitWithError('$exception');
|
||||
}
|
||||
@@ -586,62 +643,71 @@ void main(List<String> rawArgs) {
|
||||
removeUndefinedLocalizations(cupertinoLocaleToResources);
|
||||
}
|
||||
|
||||
final String? widgetsLocalizations = options.writeToFile || !options.cupertinoOnly
|
||||
? generateArbBasedLocalizationSubclasses(
|
||||
localeToResources: widgetsLocaleToResources,
|
||||
localeToResourceAttributes: widgetsLocaleToResourceAttributes,
|
||||
generatedClassPrefix: 'WidgetsLocalization',
|
||||
baseClass: 'GlobalWidgetsLocalizations',
|
||||
generateHeader: generateWidgetsHeader,
|
||||
generateConstructor: generateWidgetsConstructor,
|
||||
generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass,
|
||||
factoryName: widgetsFactoryName,
|
||||
factoryDeclaration: widgetsFactoryDeclaration,
|
||||
callsFactoryWithConst: true,
|
||||
factoryArguments: widgetsFactoryArguments,
|
||||
supportedLanguagesConstant: widgetsSupportedLanguagesConstant,
|
||||
supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro,
|
||||
)
|
||||
: null;
|
||||
final String? materialLocalizations = options.writeToFile || !options.cupertinoOnly
|
||||
? generateArbBasedLocalizationSubclasses(
|
||||
localeToResources: materialLocaleToResources,
|
||||
localeToResourceAttributes: materialLocaleToResourceAttributes,
|
||||
generatedClassPrefix: 'MaterialLocalization',
|
||||
baseClass: 'GlobalMaterialLocalizations',
|
||||
generateHeader: generateMaterialHeader,
|
||||
generateConstructor: generateMaterialConstructor,
|
||||
factoryName: materialFactoryName,
|
||||
factoryDeclaration: materialFactoryDeclaration,
|
||||
callsFactoryWithConst: false,
|
||||
factoryArguments: materialFactoryArguments,
|
||||
supportedLanguagesConstant: materialSupportedLanguagesConstant,
|
||||
supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
|
||||
)
|
||||
: null;
|
||||
final String? cupertinoLocalizations = options.writeToFile || !options.materialOnly
|
||||
? generateArbBasedLocalizationSubclasses(
|
||||
localeToResources: cupertinoLocaleToResources,
|
||||
localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
|
||||
generatedClassPrefix: 'CupertinoLocalization',
|
||||
baseClass: 'GlobalCupertinoLocalizations',
|
||||
generateHeader: generateCupertinoHeader,
|
||||
generateConstructor: generateCupertinoConstructor,
|
||||
factoryName: cupertinoFactoryName,
|
||||
factoryDeclaration: cupertinoFactoryDeclaration,
|
||||
callsFactoryWithConst: false,
|
||||
factoryArguments: cupertinoFactoryArguments,
|
||||
supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
|
||||
supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
|
||||
)
|
||||
: null;
|
||||
final String? widgetsLocalizations =
|
||||
options.writeToFile || !options.cupertinoOnly
|
||||
? generateArbBasedLocalizationSubclasses(
|
||||
localeToResources: widgetsLocaleToResources,
|
||||
localeToResourceAttributes: widgetsLocaleToResourceAttributes,
|
||||
generatedClassPrefix: 'WidgetsLocalization',
|
||||
baseClass: 'GlobalWidgetsLocalizations',
|
||||
generateHeader: generateWidgetsHeader,
|
||||
generateConstructor: generateWidgetsConstructor,
|
||||
generateConstructorForCountrySubClass: generateWidgetsConstructorForCountrySubclass,
|
||||
factoryName: widgetsFactoryName,
|
||||
factoryDeclaration: widgetsFactoryDeclaration,
|
||||
callsFactoryWithConst: true,
|
||||
factoryArguments: widgetsFactoryArguments,
|
||||
supportedLanguagesConstant: widgetsSupportedLanguagesConstant,
|
||||
supportedLanguagesDocMacro: widgetsSupportedLanguagesDocMacro,
|
||||
)
|
||||
: null;
|
||||
final String? materialLocalizations =
|
||||
options.writeToFile || !options.cupertinoOnly
|
||||
? generateArbBasedLocalizationSubclasses(
|
||||
localeToResources: materialLocaleToResources,
|
||||
localeToResourceAttributes: materialLocaleToResourceAttributes,
|
||||
generatedClassPrefix: 'MaterialLocalization',
|
||||
baseClass: 'GlobalMaterialLocalizations',
|
||||
generateHeader: generateMaterialHeader,
|
||||
generateConstructor: generateMaterialConstructor,
|
||||
factoryName: materialFactoryName,
|
||||
factoryDeclaration: materialFactoryDeclaration,
|
||||
callsFactoryWithConst: false,
|
||||
factoryArguments: materialFactoryArguments,
|
||||
supportedLanguagesConstant: materialSupportedLanguagesConstant,
|
||||
supportedLanguagesDocMacro: materialSupportedLanguagesDocMacro,
|
||||
)
|
||||
: null;
|
||||
final String? cupertinoLocalizations =
|
||||
options.writeToFile || !options.materialOnly
|
||||
? generateArbBasedLocalizationSubclasses(
|
||||
localeToResources: cupertinoLocaleToResources,
|
||||
localeToResourceAttributes: cupertinoLocaleToResourceAttributes,
|
||||
generatedClassPrefix: 'CupertinoLocalization',
|
||||
baseClass: 'GlobalCupertinoLocalizations',
|
||||
generateHeader: generateCupertinoHeader,
|
||||
generateConstructor: generateCupertinoConstructor,
|
||||
factoryName: cupertinoFactoryName,
|
||||
factoryDeclaration: cupertinoFactoryDeclaration,
|
||||
callsFactoryWithConst: false,
|
||||
factoryArguments: cupertinoFactoryArguments,
|
||||
supportedLanguagesConstant: cupertinoSupportedLanguagesConstant,
|
||||
supportedLanguagesDocMacro: cupertinoSupportedLanguagesDocMacro,
|
||||
)
|
||||
: null;
|
||||
|
||||
if (options.writeToFile) {
|
||||
final File widgetsLocalizationsFile = File(path.join(directory.path, 'generated_widgets_localizations.dart'));
|
||||
final File widgetsLocalizationsFile = File(
|
||||
path.join(directory.path, 'generated_widgets_localizations.dart'),
|
||||
);
|
||||
widgetsLocalizationsFile.writeAsStringSync(widgetsLocalizations!, flush: true);
|
||||
final File materialLocalizationsFile = File(path.join(directory.path, 'generated_material_localizations.dart'));
|
||||
final File materialLocalizationsFile = File(
|
||||
path.join(directory.path, 'generated_material_localizations.dart'),
|
||||
);
|
||||
materialLocalizationsFile.writeAsStringSync(materialLocalizations!, flush: true);
|
||||
final File cupertinoLocalizationsFile = File(path.join(directory.path, 'generated_cupertino_localizations.dart'));
|
||||
final File cupertinoLocalizationsFile = File(
|
||||
path.join(directory.path, 'generated_cupertino_localizations.dart'),
|
||||
);
|
||||
cupertinoLocalizationsFile.writeAsStringSync(cupertinoLocalizations!, flush: true);
|
||||
} else {
|
||||
if (options.cupertinoOnly) {
|
||||
|
||||
@@ -33,7 +33,13 @@ Future<void> main(List<String> rawArgs) async {
|
||||
}
|
||||
checkCwdIsRepoRoot('gen_missing_localizations');
|
||||
|
||||
final String localizationPath = path.join('packages', 'flutter_localizations', 'lib', 'src', 'l10n');
|
||||
final String localizationPath = path.join(
|
||||
'packages',
|
||||
'flutter_localizations',
|
||||
'lib',
|
||||
'src',
|
||||
'l10n',
|
||||
);
|
||||
updateMissingResources(localizationPath, 'material', removeUndefined: removeUndefined);
|
||||
updateMissingResources(localizationPath, 'cupertino', removeUndefined: removeUndefined);
|
||||
}
|
||||
@@ -58,7 +64,7 @@ void writeBundle(File file, Map<String, dynamic> bundle) {
|
||||
Set<String> resourceKeys(Map<String, dynamic> bundle) {
|
||||
return Set<String>.from(
|
||||
// Skip any attribute keys
|
||||
bundle.keys.where((String key) => !key.startsWith('@'))
|
||||
bundle.keys.where((String key) => !key.startsWith('@')),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,14 +85,21 @@ bool isPluralVariation(String key, Map<String, dynamic> bundle) {
|
||||
return bundle.containsKey('${prefix}Other');
|
||||
}
|
||||
|
||||
void updateMissingResources(String localizationPath, String groupPrefix, {bool removeUndefined = false}) {
|
||||
void updateMissingResources(
|
||||
String localizationPath,
|
||||
String groupPrefix, {
|
||||
bool removeUndefined = false,
|
||||
}) {
|
||||
final Directory localizationDir = Directory(localizationPath);
|
||||
final RegExp filenamePattern = RegExp('${groupPrefix}_(\\w+)\\.arb');
|
||||
|
||||
final Map<String, dynamic> englishBundle = loadBundle(File(path.join(localizationPath, '${groupPrefix}_en.arb')));
|
||||
final Map<String, dynamic> englishBundle = loadBundle(
|
||||
File(path.join(localizationPath, '${groupPrefix}_en.arb')),
|
||||
);
|
||||
final Set<String> requiredKeys = resourceKeys(englishBundle);
|
||||
|
||||
for (final FileSystemEntity entity in localizationDir.listSync().toList()..sort(sortFilesByPath)) {
|
||||
for (final FileSystemEntity entity
|
||||
in localizationDir.listSync().toList()..sort(sortFilesByPath)) {
|
||||
final String entityPath = entity.path;
|
||||
if (FileSystemEntity.isFileSync(entityPath) && filenamePattern.hasMatch(entityPath)) {
|
||||
final String localeString = filenamePattern.firstMatch(entityPath)![1]!;
|
||||
@@ -105,17 +118,15 @@ void updateMissingResources(String localizationPath, String groupPrefix, {bool r
|
||||
// --remove-undefined is passed.
|
||||
if (removeUndefined) {
|
||||
bool isIncluded(String key) {
|
||||
return !isPluralVariation(key, localeBundle)
|
||||
&& !intentionallyOmitted(key, localeBundle);
|
||||
return !isPluralVariation(key, localeBundle) &&
|
||||
!intentionallyOmitted(key, localeBundle);
|
||||
}
|
||||
|
||||
// Find any resources in this locale that don't appear in the
|
||||
// canonical locale, and skipping any which should not be included
|
||||
// (plurals and intentionally omitted).
|
||||
final Set<String> extraResources = localeResources
|
||||
.difference(requiredKeys)
|
||||
.where(isIncluded)
|
||||
.toSet();
|
||||
final Set<String> extraResources =
|
||||
localeResources.difference(requiredKeys).where(isIncluded).toSet();
|
||||
|
||||
// Remove them.
|
||||
localeBundle.removeWhere((String key, dynamic value) {
|
||||
@@ -132,12 +143,21 @@ void updateMissingResources(String localizationPath, String groupPrefix, {bool r
|
||||
|
||||
// Add in any resources that are in the canonical locale and not present
|
||||
// in this locale.
|
||||
final Set<String> missingResources = requiredKeys.difference(localeResources).where(
|
||||
(String key) => !isPluralVariation(key, localeBundle) && !intentionallyOmitted(key, localeBundle)
|
||||
).toSet();
|
||||
final Set<String> missingResources =
|
||||
requiredKeys
|
||||
.difference(localeResources)
|
||||
.where(
|
||||
(String key) =>
|
||||
!isPluralVariation(key, localeBundle) &&
|
||||
!intentionallyOmitted(key, localeBundle),
|
||||
)
|
||||
.toSet();
|
||||
if (missingResources.isNotEmpty) {
|
||||
localeBundle.addEntries(missingResources.map((String k) =>
|
||||
MapEntry<String, String>(k, englishBundle[k].toString())));
|
||||
localeBundle.addEntries(
|
||||
missingResources.map(
|
||||
(String k) => MapEntry<String, String>(k, englishBundle[k].toString()),
|
||||
),
|
||||
);
|
||||
shouldWrite = true;
|
||||
print('Updating $entityPath with missing entries for $missingResources');
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
const String registry = 'https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
|
||||
const String registry =
|
||||
'https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry';
|
||||
|
||||
/// A script to generate a Dart cache of https://www.iana.org. This should be
|
||||
/// run occasionally. It was created since iana.org was found to be flaky.
|
||||
@@ -15,9 +16,12 @@ Future<void> main() async {
|
||||
final HttpClient client = HttpClient();
|
||||
final HttpClientRequest request = await client.getUrl(Uri.parse(registry));
|
||||
final HttpClientResponse response = await request.close();
|
||||
final String body = (await response.cast<List<int>>().transform<String>(utf8.decoder).toList()).join();
|
||||
final String body =
|
||||
(await response.cast<List<int>>().transform<String>(utf8.decoder).toList()).join();
|
||||
final File subtagRegistry = File('../language_subtag_registry.dart');
|
||||
final File subtagRegistryFlutterTools = File('../../../../packages/flutter_tools/lib/src/localizations/language_subtag_registry.dart');
|
||||
final File subtagRegistryFlutterTools = File(
|
||||
'../../../../packages/flutter_tools/lib/src/localizations/language_subtag_registry.dart',
|
||||
);
|
||||
|
||||
final String content = '''
|
||||
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||
@@ -27,7 +31,6 @@ Future<void> main() async {
|
||||
/// Cache of $registry.
|
||||
const String languageSubtagRegistry = \'\'\'$body\'\'\';''';
|
||||
|
||||
|
||||
subtagRegistry.writeAsStringSync(content);
|
||||
subtagRegistryFlutterTools.writeAsStringSync(content);
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ import '../cupertino_localizations.dart';
|
||||
//
|
||||
// These classes are constructed by the [getCupertinoTranslation] method at the
|
||||
// bottom of this file, and used by the [_GlobalCupertinoLocalizationsDelegate.load]
|
||||
// method defined in `flutter_localizations/lib/src/cupertino_localizations.dart`.''';
|
||||
// method defined in `flutter_localizations/lib/src/cupertino_localizations.dart`.
|
||||
|
||||
// TODO(goderbauer): Extend the generator to properly format the output.
|
||||
// dart format off''';
|
||||
}
|
||||
|
||||
/// Returns the source of the constructor for a GlobalCupertinoLocalizations
|
||||
|
||||
@@ -26,7 +26,10 @@ import '../material_localizations.dart';
|
||||
//
|
||||
// These classes are constructed by the [getMaterialTranslation] method at the
|
||||
// bottom of this file, and used by the [_MaterialLocalizationsDelegate.load]
|
||||
// method defined in `flutter_localizations/lib/src/material_localizations.dart`.''';
|
||||
// method defined in `flutter_localizations/lib/src/material_localizations.dart`.
|
||||
|
||||
// TODO(goderbauer): Extend the generator to properly format the output.
|
||||
// dart format off''';
|
||||
}
|
||||
|
||||
/// Returns the source of the constructor for a GlobalMaterialLocalizations
|
||||
|
||||
@@ -33,7 +33,10 @@ import '../widgets_localizations.dart';
|
||||
//
|
||||
// These classes are constructed by the [getWidgetsTranslation] method at the
|
||||
// bottom of this file, and used by the [_WidgetsLocalizationsDelegate.load]
|
||||
// method defined in `flutter_localizations/lib/src/widgets_localizations.dart`.''';
|
||||
// method defined in `flutter_localizations/lib/src/widgets_localizations.dart`.
|
||||
|
||||
// TODO(goderbauer): Extend the generator to properly format the output.
|
||||
// dart format off''';
|
||||
}
|
||||
|
||||
/// Returns the source of the constructor for a GlobalWidgetsLocalizations
|
||||
@@ -41,7 +44,8 @@ import '../widgets_localizations.dart';
|
||||
String generateWidgetsConstructor(LocaleInfo locale) {
|
||||
final String localeName = locale.originalString;
|
||||
final String language = locale.languageCode.toLowerCase();
|
||||
final String textDirection = _rtlLanguages.contains(language) ? 'TextDirection.rtl' : 'TextDirection.ltr';
|
||||
final String textDirection =
|
||||
_rtlLanguages.contains(language) ? 'TextDirection.rtl' : 'TextDirection.ltr';
|
||||
return '''
|
||||
/// Create an instance of the translation bundle for ${describeLocale(localeName)}.
|
||||
///
|
||||
|
||||
@@ -13,7 +13,7 @@ import 'language_subtag_registry.dart';
|
||||
typedef HeaderGenerator = String Function(String regenerateInstructions);
|
||||
typedef ConstructorGenerator = String Function(LocaleInfo locale);
|
||||
|
||||
int sortFilesByPath (FileSystemEntity a, FileSystemEntity b) {
|
||||
int sortFilesByPath(FileSystemEntity a, FileSystemEntity b) {
|
||||
return a.path.compareTo(b.path);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class LocaleInfo implements Comparable<LocaleInfo> {
|
||||
///
|
||||
/// When `deriveScriptCode` is true, if [scriptCode] was unspecified, it will
|
||||
/// be derived from the [languageCode] and [countryCode] if possible.
|
||||
factory LocaleInfo.fromString(String locale, { bool deriveScriptCode = false }) {
|
||||
factory LocaleInfo.fromString(String locale, {bool deriveScriptCode = false}) {
|
||||
final List<String> codes = locale.split('_'); // [language, script, country]
|
||||
assert(codes.isNotEmpty && codes.length < 4);
|
||||
final String languageCode = codes[0];
|
||||
@@ -94,20 +94,21 @@ class LocaleInfo implements Comparable<LocaleInfo> {
|
||||
final String languageCode;
|
||||
final String? scriptCode;
|
||||
final String? countryCode;
|
||||
final int length; // The number of fields. Ranges from 1-3.
|
||||
final String originalString; // Original un-parsed locale string.
|
||||
final int length; // The number of fields. Ranges from 1-3.
|
||||
final String originalString; // Original un-parsed locale string.
|
||||
|
||||
String camelCase() {
|
||||
return originalString
|
||||
.split('_')
|
||||
.map<String>((String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase())
|
||||
.join();
|
||||
.split('_')
|
||||
.map<String>(
|
||||
(String part) => part.substring(0, 1).toUpperCase() + part.substring(1).toLowerCase(),
|
||||
)
|
||||
.join();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LocaleInfo
|
||||
&& other.originalString == originalString;
|
||||
return other is LocaleInfo && other.originalString == originalString;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -132,7 +133,6 @@ void loadMatchingArbsIntoBundleMaps({
|
||||
required Map<LocaleInfo, Map<String, String>> localeToResources,
|
||||
required Map<LocaleInfo, Map<String, dynamic>> localeToResourceAttributes,
|
||||
}) {
|
||||
|
||||
/// Set that holds the locales that were assumed from the existing locales.
|
||||
///
|
||||
/// For example, when the data lacks data for zh_Hant, we will use the data of
|
||||
@@ -151,7 +151,8 @@ void loadMatchingArbsIntoBundleMaps({
|
||||
void populateResources(LocaleInfo locale, File file) {
|
||||
final Map<String, String> resources = localeToResources[locale]!;
|
||||
final Map<String, dynamic> attributes = localeToResourceAttributes[locale]!;
|
||||
final Map<String, dynamic> bundle = json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
final Map<String, dynamic> bundle =
|
||||
json.decode(file.readAsStringSync()) as Map<String, dynamic>;
|
||||
for (final String key in bundle.keys) {
|
||||
// The ARB file resource "attributes" for foo are called @foo.
|
||||
if (key.startsWith('@')) {
|
||||
@@ -161,10 +162,14 @@ void loadMatchingArbsIntoBundleMaps({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only pre-assume scriptCode if there is a country or script code to assume off of.
|
||||
// When we assume scriptCode based on languageCode-only, we want this initial pass
|
||||
// to use the un-assumed version as a base class.
|
||||
LocaleInfo locale = LocaleInfo.fromString(localeString, deriveScriptCode: localeString.split('_').length > 1);
|
||||
LocaleInfo locale = LocaleInfo.fromString(
|
||||
localeString,
|
||||
deriveScriptCode: localeString.split('_').length > 1,
|
||||
);
|
||||
// Allow overwrite if the existing data is assumed.
|
||||
if (assumedLocales.contains(locale)) {
|
||||
localeToResources[locale] = <String, String>{};
|
||||
@@ -178,7 +183,9 @@ void loadMatchingArbsIntoBundleMaps({
|
||||
// Add an assumed locale to default to when there is no info on scriptOnly locales.
|
||||
locale = LocaleInfo.fromString(localeString, deriveScriptCode: true);
|
||||
if (locale.scriptCode != null) {
|
||||
final LocaleInfo scriptLocale = LocaleInfo.fromString('${locale.languageCode}_${locale.scriptCode}');
|
||||
final LocaleInfo scriptLocale = LocaleInfo.fromString(
|
||||
'${locale.languageCode}_${locale.scriptCode}',
|
||||
);
|
||||
if (!localeToResources.containsKey(scriptLocale)) {
|
||||
assumedLocales.add(scriptLocale);
|
||||
localeToResources[scriptLocale] ??= <String, String>{};
|
||||
@@ -201,39 +208,35 @@ void checkCwdIsRepoRoot(String commandName) {
|
||||
if (!isRepoRoot) {
|
||||
exitWithError(
|
||||
'$commandName must be run from the root of the Flutter repository. The '
|
||||
'current working directory is: ${Directory.current.path}'
|
||||
'current working directory is: ${Directory.current.path}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GeneratorOptions parseArgs(List<String> rawArgs) {
|
||||
final argslib.ArgParser argParser = argslib.ArgParser()
|
||||
..addFlag(
|
||||
'help',
|
||||
abbr: 'h',
|
||||
help: 'Print the usage message for this command',
|
||||
)
|
||||
..addFlag(
|
||||
'overwrite',
|
||||
abbr: 'w',
|
||||
help: 'Overwrite existing localizations',
|
||||
)
|
||||
..addFlag(
|
||||
'remove-undefined',
|
||||
help: 'Remove any localizations that are not defined in the canonical locale.',
|
||||
)
|
||||
..addFlag(
|
||||
'widgets',
|
||||
help: 'Whether to print the generated classes for the Widgets package only. Ignored when --overwrite is passed.',
|
||||
)
|
||||
..addFlag(
|
||||
'material',
|
||||
help: 'Whether to print the generated classes for the Material package only. Ignored when --overwrite is passed.',
|
||||
)
|
||||
..addFlag(
|
||||
'cupertino',
|
||||
help: 'Whether to print the generated classes for the Cupertino package only. Ignored when --overwrite is passed.',
|
||||
);
|
||||
final argslib.ArgParser argParser =
|
||||
argslib.ArgParser()
|
||||
..addFlag('help', abbr: 'h', help: 'Print the usage message for this command')
|
||||
..addFlag('overwrite', abbr: 'w', help: 'Overwrite existing localizations')
|
||||
..addFlag(
|
||||
'remove-undefined',
|
||||
help: 'Remove any localizations that are not defined in the canonical locale.',
|
||||
)
|
||||
..addFlag(
|
||||
'widgets',
|
||||
help:
|
||||
'Whether to print the generated classes for the Widgets package only. Ignored when --overwrite is passed.',
|
||||
)
|
||||
..addFlag(
|
||||
'material',
|
||||
help:
|
||||
'Whether to print the generated classes for the Material package only. Ignored when --overwrite is passed.',
|
||||
)
|
||||
..addFlag(
|
||||
'cupertino',
|
||||
help:
|
||||
'Whether to print the generated classes for the Cupertino package only. Ignored when --overwrite is passed.',
|
||||
);
|
||||
final argslib.ArgResults args = argParser.parse(rawArgs);
|
||||
if (args.wasParsed('help') && args['help'] == true) {
|
||||
stderr.writeln(argParser.usage);
|
||||
@@ -305,12 +308,19 @@ const String kParentheticalPrefix = ' (';
|
||||
/// The data is obtained from the official IANA registry.
|
||||
void precacheLanguageAndRegionTags() {
|
||||
final List<Map<String, List<String>>> sections =
|
||||
languageSubtagRegistry.split('%%').skip(1).map<Map<String, List<String>>>(_parseSection).toList();
|
||||
languageSubtagRegistry
|
||||
.split('%%')
|
||||
.skip(1)
|
||||
.map<Map<String, List<String>>>(_parseSection)
|
||||
.toList();
|
||||
for (final Map<String, List<String>> section in sections) {
|
||||
assert(section.containsKey('Type'), section.toString());
|
||||
final String type = section['Type']!.single;
|
||||
if (type == 'language' || type == 'region' || type == 'script') {
|
||||
assert(section.containsKey('Subtag') && section.containsKey('Description'), section.toString());
|
||||
assert(
|
||||
section.containsKey('Subtag') && section.containsKey('Description'),
|
||||
section.toString(),
|
||||
);
|
||||
final String subtag = section['Subtag']!.single;
|
||||
String description = section['Description']!.join(' ');
|
||||
if (description.startsWith('United ')) {
|
||||
@@ -364,11 +374,7 @@ String describeLocale(String tag) {
|
||||
}
|
||||
|
||||
/// Writes the header of each class which corresponds to a locale.
|
||||
String generateClassDeclaration(
|
||||
LocaleInfo locale,
|
||||
String classNamePrefix,
|
||||
String superClass,
|
||||
) {
|
||||
String generateClassDeclaration(LocaleInfo locale, String classNamePrefix, String superClass) {
|
||||
final String camelCaseName = locale.camelCase();
|
||||
return '''
|
||||
|
||||
@@ -417,23 +423,23 @@ String generateString(String value) {
|
||||
!value.contains(backslash),
|
||||
'Input string cannot contain the sequence: '
|
||||
'"__BACKSLASH__", as it is used as part of '
|
||||
'backslash character processing.'
|
||||
'backslash character processing.',
|
||||
);
|
||||
|
||||
value = value
|
||||
// Replace backslashes with a placeholder for now to properly parse
|
||||
// other special characters.
|
||||
.replaceAll(r'\', backslash)
|
||||
.replaceAll(r'$', r'\$')
|
||||
.replaceAll("'", r"\'")
|
||||
.replaceAll('"', r'\"')
|
||||
.replaceAll('\n', r'\n')
|
||||
.replaceAll('\f', r'\f')
|
||||
.replaceAll('\t', r'\t')
|
||||
.replaceAll('\r', r'\r')
|
||||
.replaceAll('\b', r'\b')
|
||||
// Reintroduce escaped backslashes into generated Dart string.
|
||||
.replaceAll(backslash, r'\\');
|
||||
// Replace backslashes with a placeholder for now to properly parse
|
||||
// other special characters.
|
||||
.replaceAll(r'\', backslash)
|
||||
.replaceAll(r'$', r'\$')
|
||||
.replaceAll("'", r"\'")
|
||||
.replaceAll('"', r'\"')
|
||||
.replaceAll('\n', r'\n')
|
||||
.replaceAll('\f', r'\f')
|
||||
.replaceAll('\t', r'\t')
|
||||
.replaceAll('\r', r'\r')
|
||||
.replaceAll('\b', r'\b')
|
||||
// Reintroduce escaped backslashes into generated Dart string.
|
||||
.replaceAll(backslash, r'\\');
|
||||
|
||||
return "'$value'";
|
||||
}
|
||||
@@ -446,6 +452,7 @@ String generateEncodedString(String? locale, String value) {
|
||||
return generateString(value);
|
||||
}
|
||||
|
||||
final String unicodeEscapes = value.runes.map((int code) => '\\u{${code.toRadixString(16)}}').join();
|
||||
final String unicodeEscapes =
|
||||
value.runes.map((int code) => '\\u{${code.toRadixString(16)}}').join();
|
||||
return "'$unicodeEscapes'";
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const List<String> kPluralSuffixes = <String>['Other', 'Zero', 'One', 'Two', 'Fe
|
||||
final RegExp kPluralRegexp = RegExp(r'(\w*)(' + kPluralSuffixes.skip(1).join(r'|') + r')$');
|
||||
|
||||
class ValidationError implements Exception {
|
||||
ValidationError(this. message);
|
||||
ValidationError(this.message);
|
||||
final String message;
|
||||
@override
|
||||
String toString() => message;
|
||||
@@ -51,6 +51,7 @@ void validateEnglishLocalizations(File file) {
|
||||
final int suffixIndex = resourceId.indexOf(suffix);
|
||||
return suffixIndex != -1 && bundle['@${resourceId.substring(0, suffixIndex)}'] != null;
|
||||
}
|
||||
|
||||
if (kPluralSuffixes.any(checkPluralResource)) {
|
||||
continue;
|
||||
}
|
||||
@@ -107,10 +108,9 @@ void validateEnglishLocalizations(File file) {
|
||||
/// keys in the resources for this locale, but which _aren't_ keys in the
|
||||
/// canonical list.
|
||||
/// 5. Removes the invalid mappings from this resource's locale.
|
||||
void removeUndefinedLocalizations(
|
||||
Map<LocaleInfo, Map<String, String>> localeToResources,
|
||||
) {
|
||||
final Map<String, String> canonicalLocalizations = localeToResources[LocaleInfo.fromString('en')]!;
|
||||
void removeUndefinedLocalizations(Map<LocaleInfo, Map<String, String>> localeToResources) {
|
||||
final Map<String, String> canonicalLocalizations =
|
||||
localeToResources[LocaleInfo.fromString('en')]!;
|
||||
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
|
||||
|
||||
localeToResources.forEach((LocaleInfo locale, Map<String, String> resources) {
|
||||
@@ -124,7 +124,7 @@ void removeUndefinedLocalizations(
|
||||
}
|
||||
|
||||
final Set<String> keys = Set<String>.from(
|
||||
resources.keys.where((String key) => !isPluralVariation(key))
|
||||
resources.keys.where((String key) => !isPluralVariation(key)),
|
||||
);
|
||||
|
||||
final Set<String> invalidKeys = keys.difference(canonicalKeys);
|
||||
@@ -146,7 +146,8 @@ void validateLocalizations(
|
||||
Map<LocaleInfo, Map<String, dynamic>> localeToAttributes, {
|
||||
bool removeUndefined = false,
|
||||
}) {
|
||||
final Map<String, String> canonicalLocalizations = localeToResources[LocaleInfo.fromString('en')]!;
|
||||
final Map<String, String> canonicalLocalizations =
|
||||
localeToResources[LocaleInfo.fromString('en')]!;
|
||||
final Set<String> canonicalKeys = Set<String>.from(canonicalLocalizations.keys);
|
||||
final StringBuffer errorMessages = StringBuffer();
|
||||
bool explainMissingKeys = false;
|
||||
@@ -168,14 +169,16 @@ void validateLocalizations(
|
||||
}
|
||||
|
||||
final Set<String> keys = Set<String>.from(
|
||||
resources.keys.where((String key) => !isPluralVariation(key))
|
||||
resources.keys.where((String key) => !isPluralVariation(key)),
|
||||
);
|
||||
|
||||
// Make sure keys are valid (i.e. they also exist in the canonical
|
||||
// localizations)
|
||||
final Set<String> invalidKeys = keys.difference(canonicalKeys);
|
||||
if (invalidKeys.isNotEmpty && !removeUndefined) {
|
||||
errorMessages.writeln('Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}');
|
||||
errorMessages.writeln(
|
||||
'Locale "$locale" contains invalid resource keys: ${invalidKeys.join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
// For language-level locales only, check that they have a complete list of
|
||||
@@ -183,7 +186,7 @@ void validateLocalizations(
|
||||
if (locale.length == 1) {
|
||||
final Map<String, dynamic>? attributes = localeToAttributes[locale];
|
||||
final List<String?> missingKeys = <String?>[];
|
||||
for (final String missingKey in canonicalKeys.difference(keys)) {
|
||||
for (final String missingKey in canonicalKeys.difference(keys)) {
|
||||
final dynamic attribute = attributes?[missingKey];
|
||||
final bool intentionallyOmitted = attribute is Map && attribute.containsKey('notUsed');
|
||||
if (!intentionallyOmitted && !isPluralVariation(missingKey)) {
|
||||
@@ -192,23 +195,25 @@ void validateLocalizations(
|
||||
}
|
||||
if (missingKeys.isNotEmpty) {
|
||||
explainMissingKeys = true;
|
||||
errorMessages.writeln('Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}');
|
||||
errorMessages.writeln(
|
||||
'Locale "$locale" is missing the following resource keys: ${missingKeys.join(', ')}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.isNotEmpty) {
|
||||
if (explainMissingKeys) {
|
||||
errorMessages
|
||||
..writeln()
|
||||
..writeln(
|
||||
'If a resource key is intentionally omitted, add an attribute corresponding '
|
||||
'to the key name with a "notUsed" property explaining why. Example:'
|
||||
)
|
||||
..writeln()
|
||||
..writeln('"@anteMeridiemAbbreviation": {')
|
||||
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
|
||||
..writeln('}');
|
||||
errorMessages
|
||||
..writeln()
|
||||
..writeln(
|
||||
'If a resource key is intentionally omitted, add an attribute corresponding '
|
||||
'to the key name with a "notUsed" property explaining why. Example:',
|
||||
)
|
||||
..writeln()
|
||||
..writeln('"@anteMeridiemAbbreviation": {')
|
||||
..writeln(' "notUsed": "Sindhi time format does not use a.m. indicator"')
|
||||
..writeln('}');
|
||||
}
|
||||
throw ValidationError(errorMessages.toString());
|
||||
}
|
||||
|
||||
@@ -62,7 +62,9 @@ void main(List<String> args) {
|
||||
print('');
|
||||
print('Stats:');
|
||||
print(' packages/flutter : ${getStatsFor(Directory("packages/flutter"))}');
|
||||
print(' dev/integration_tests/flutter_gallery : ${getStatsFor(Directory("dev/integration_tests/flutter_gallery"))}');
|
||||
print(
|
||||
' dev/integration_tests/flutter_gallery : ${getStatsFor(Directory("dev/integration_tests/flutter_gallery"))}',
|
||||
);
|
||||
|
||||
final Directory lib = _dir(out, 'lib');
|
||||
if (lib.existsSync()) {
|
||||
@@ -86,15 +88,13 @@ void main(List<String> args) {
|
||||
_file(out, 'pubspec.yaml').writeAsStringSync(pubspec);
|
||||
|
||||
// Replace the (flutter_gallery specific) analysis_options.yaml file with a default one.
|
||||
_file(out, 'analysis_options.yaml').writeAsStringSync(
|
||||
'''
|
||||
_file(out, 'analysis_options.yaml').writeAsStringSync('''
|
||||
analyzer:
|
||||
errors:
|
||||
# See analysis_options.yaml in the flutter root for context.
|
||||
deprecated_member_use: ignore
|
||||
deprecated_member_use_from_same_package: ignore
|
||||
'''
|
||||
);
|
||||
''');
|
||||
|
||||
_file(out, '.dartignore').writeAsStringSync('');
|
||||
|
||||
|
||||
@@ -45,19 +45,12 @@ final class NativeDriverCommandExtension implements CommandExtension {
|
||||
if (await _builtInCall(command.method) case final Result result) {
|
||||
return result;
|
||||
}
|
||||
final Object? result = await _channel.invokeMethod<Object>(
|
||||
command.method,
|
||||
command.arguments,
|
||||
);
|
||||
final Object? result = await _channel.invokeMethod<Object>(command.method, command.arguments);
|
||||
if (result == null) {
|
||||
return const _MethodChannelResult(<String, Object?>{});
|
||||
}
|
||||
if (result is! Map<String, Object?>) {
|
||||
throw ArgumentError.value(
|
||||
result,
|
||||
'result',
|
||||
'Expected a Map<String, Object?>',
|
||||
);
|
||||
throw ArgumentError.value(result, 'result', 'Expected a Map<String, Object?>');
|
||||
}
|
||||
return _MethodChannelResult(result);
|
||||
}
|
||||
@@ -68,14 +61,12 @@ final class NativeDriverCommandExtension implements CommandExtension {
|
||||
Future<Result?> _builtInCall(String method) async {
|
||||
switch (method) {
|
||||
case 'rotate_landscape':
|
||||
await flt.SystemChrome.setPreferredOrientations(
|
||||
const <flt.DeviceOrientation>[flt.DeviceOrientation.landscapeLeft],
|
||||
);
|
||||
await flt.SystemChrome.setPreferredOrientations(const <flt.DeviceOrientation>[
|
||||
flt.DeviceOrientation.landscapeLeft,
|
||||
]);
|
||||
return Result.empty;
|
||||
case 'rotate_default':
|
||||
await flt.SystemChrome.setPreferredOrientations(
|
||||
const <flt.DeviceOrientation>[],
|
||||
);
|
||||
await flt.SystemChrome.setPreferredOrientations(const <flt.DeviceOrientation>[]);
|
||||
return Result.empty;
|
||||
default:
|
||||
return null;
|
||||
@@ -102,11 +93,7 @@ final class NativeDriverCommandExtension implements CommandExtension {
|
||||
} else {
|
||||
final Object? intermediate = json.decode(arguments);
|
||||
if (intermediate is! Map<String, Object?>) {
|
||||
throw ArgumentError.value(
|
||||
arguments,
|
||||
'arguments',
|
||||
'Expected a Map<String, Object?>',
|
||||
);
|
||||
throw ArgumentError.value(arguments, 'arguments', 'Expected a Map<String, Object?>');
|
||||
}
|
||||
decoded = intermediate;
|
||||
}
|
||||
|
||||
@@ -20,8 +20,7 @@ import 'package:test_api/test_api.dart';
|
||||
import 'src/common.dart';
|
||||
|
||||
export 'src/backend/android.dart' show AndroidDeviceTarget, AndroidNativeDriver;
|
||||
export 'src/common.dart'
|
||||
show ByNativeAccessibilityLabel, ByNativeIntegerId, NativeFinder;
|
||||
export 'src/common.dart' show ByNativeAccessibilityLabel, ByNativeIntegerId, NativeFinder;
|
||||
|
||||
part 'src/driver.dart';
|
||||
part 'src/goldens.dart';
|
||||
|
||||
@@ -46,12 +46,8 @@ Future<void> enableSkiaGoldComparator({String? namePrefix}) async {
|
||||
'Set it to use Skia Gold.',
|
||||
);
|
||||
}
|
||||
final io.Directory tmpDir = io.Directory.systemTemp.createTempSync(
|
||||
'android_driver_test',
|
||||
);
|
||||
final bool isPresubmit = io.Platform.environment.containsKey(
|
||||
_kGoldctlPresubmitKey,
|
||||
);
|
||||
final io.Directory tmpDir = io.Directory.systemTemp.createTempSync('android_driver_test');
|
||||
final bool isPresubmit = io.Platform.environment.containsKey(_kGoldctlPresubmitKey);
|
||||
io.stderr.writeln(
|
||||
'=== Using Skia Gold ===\n'
|
||||
'Environment variable $_kGoldctlKey is set, using Skia Gold: \n'
|
||||
@@ -76,12 +72,8 @@ Future<void> enableSkiaGoldComparator({String? namePrefix}) async {
|
||||
}
|
||||
|
||||
final class _GoldenFileComparator extends GoldenFileComparator {
|
||||
_GoldenFileComparator(
|
||||
this.skiaClient, {
|
||||
required this.isPresubmit,
|
||||
this.namePrefix,
|
||||
Uri? baseDir,
|
||||
}) : baseDir = baseDir ?? Uri.parse(path.dirname(io.Platform.script.path));
|
||||
_GoldenFileComparator(this.skiaClient, {required this.isPresubmit, this.namePrefix, Uri? baseDir})
|
||||
: baseDir = baseDir ?? Uri.parse(path.dirname(io.Platform.script.path));
|
||||
|
||||
final Uri baseDir;
|
||||
final SkiaGoldClient skiaClient;
|
||||
@@ -104,14 +96,10 @@ final class _GoldenFileComparator extends GoldenFileComparator {
|
||||
_localFs.file(goldenFile.path),
|
||||
);
|
||||
if (result != null) {
|
||||
io.stderr.writeln(
|
||||
'Skia Gold detected an error when comparing "$golden":\n\n$result',
|
||||
);
|
||||
io.stderr.writeln('Skia Gold detected an error when comparing "$golden":\n\n$result');
|
||||
io.stderr.writeln('Still succeeding, will be triaged in Flutter Gold');
|
||||
} else {
|
||||
io.stderr.writeln(
|
||||
'Skia Gold comparison succeeded comparing "$golden".',
|
||||
);
|
||||
io.stderr.writeln('Skia Gold comparison succeeded comparing "$golden".');
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
@@ -121,9 +109,7 @@ final class _GoldenFileComparator extends GoldenFileComparator {
|
||||
|
||||
@override
|
||||
Future<io.File> update(Uri golden, Uint8List imageBytes) async {
|
||||
io.stderr.writeln(
|
||||
'Updating golden file: $golden (${imageBytes.length} bytes)...',
|
||||
);
|
||||
io.stderr.writeln('Updating golden file: $golden (${imageBytes.length} bytes)...');
|
||||
final io.File goldenFile = _getGoldenFile(golden);
|
||||
await goldenFile.parent.create(recursive: true);
|
||||
await goldenFile.writeAsBytes(imageBytes, flush: true);
|
||||
@@ -140,10 +126,12 @@ final class _GoldenFileComparator extends GoldenFileComparator {
|
||||
'Golden files in the Flutter framework must end with the file extension '
|
||||
'.png.',
|
||||
);
|
||||
return Uri.parse(<String>[
|
||||
if (namePrefix != null) namePrefix!,
|
||||
baseDir.pathSegments[baseDir.pathSegments.length - 2],
|
||||
golden.toString(),
|
||||
].join('.'));
|
||||
return Uri.parse(
|
||||
<String>[
|
||||
if (namePrefix != null) namePrefix!,
|
||||
baseDir.pathSegments[baseDir.pathSegments.length - 2],
|
||||
golden.toString(),
|
||||
].join('.'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ class Adb {
|
||||
}) async {
|
||||
target ??= const AndroidDeviceTarget.onlyEmulatorOrDevice();
|
||||
final String tool = adbPath ?? 'adb';
|
||||
final Adb adb = Adb._(
|
||||
<String>[tool, ...target._toAdbArgs()],
|
||||
processManager ?? const LocalProcessManager(),
|
||||
);
|
||||
final Adb adb = Adb._(<String>[
|
||||
tool,
|
||||
...target._toAdbArgs(),
|
||||
], processManager ?? const LocalProcessManager());
|
||||
final (bool connected, String? error) = await adb.isDeviceConnected();
|
||||
if (!connected) {
|
||||
throw StateError('No device connected: $error');
|
||||
@@ -41,12 +41,7 @@ class Adb {
|
||||
}
|
||||
|
||||
Future<AdbStringResult> _runString(List<String> args) async {
|
||||
final io.ProcessResult result = await _process.run(
|
||||
<String>[
|
||||
..._prefixArgs,
|
||||
...args,
|
||||
],
|
||||
);
|
||||
final io.ProcessResult result = await _process.run(<String>[..._prefixArgs, ...args]);
|
||||
return AdbStringResult(
|
||||
result.stdout as String,
|
||||
exitCode: result.exitCode,
|
||||
@@ -55,13 +50,10 @@ class Adb {
|
||||
}
|
||||
|
||||
Future<AdbBinaryResult> _runBinary(List<String> args) async {
|
||||
final io.ProcessResult result = await _process.run(
|
||||
<String>[
|
||||
..._prefixArgs,
|
||||
...args,
|
||||
],
|
||||
stdoutEncoding: null,
|
||||
);
|
||||
final io.ProcessResult result = await _process.run(<String>[
|
||||
..._prefixArgs,
|
||||
...args,
|
||||
], stdoutEncoding: null);
|
||||
return AdbBinaryResult(
|
||||
result.stdout as Uint8List,
|
||||
exitCode: result.exitCode,
|
||||
@@ -78,11 +70,7 @@ class Adb {
|
||||
/// and an error message if the device is not connected. If the device is
|
||||
/// connected, the error message is `null`.
|
||||
Future<(bool connected, String? error)> isDeviceConnected() async {
|
||||
final AdbStringResult result = await _runString(<String>[
|
||||
'shell',
|
||||
'echo',
|
||||
'connected',
|
||||
]);
|
||||
final AdbStringResult result = await _runString(<String>['shell', 'echo', 'connected']);
|
||||
if (result.exitCode != 0) {
|
||||
return (false, result.stderr);
|
||||
} else {
|
||||
@@ -92,11 +80,7 @@ class Adb {
|
||||
|
||||
/// Takes a screenshot of the device.
|
||||
Future<Uint8List> screencap() async {
|
||||
final AdbBinaryResult result = await _runBinary(<String>[
|
||||
'exec-out',
|
||||
'screencap',
|
||||
'-p',
|
||||
]);
|
||||
final AdbBinaryResult result = await _runBinary(<String>['exec-out', 'screencap', '-p']);
|
||||
if (result.exitCode != 0) {
|
||||
throw StateError('Failed to take screenshot: ${result.stderr}');
|
||||
}
|
||||
@@ -105,13 +89,7 @@ class Adb {
|
||||
|
||||
/// Taps on the screen at the given [x] and [y] coordinates.
|
||||
Future<void> tap(int x, int y) async {
|
||||
final AdbStringResult result = await _runString(<String>[
|
||||
'shell',
|
||||
'input',
|
||||
'tap',
|
||||
'$x',
|
||||
'$y',
|
||||
]);
|
||||
final AdbStringResult result = await _runString(<String>['shell', 'input', 'tap', '$x', '$y']);
|
||||
if (result.exitCode != 0) {
|
||||
throw StateError('Failed to tap at $x, $y: ${result.stderr}');
|
||||
}
|
||||
@@ -145,10 +123,7 @@ class Adb {
|
||||
}
|
||||
|
||||
/// Resume a backgrounded app (i.e. after [sendToHome]).
|
||||
Future<void> resumeApp({
|
||||
required String appName,
|
||||
String activityName = '.MainActivity',
|
||||
}) async {
|
||||
Future<void> resumeApp({required String appName, String activityName = '.MainActivity'}) async {
|
||||
final AdbStringResult result = await _runString(<String>[
|
||||
'shell',
|
||||
'am',
|
||||
@@ -172,9 +147,7 @@ class Adb {
|
||||
'confirmed',
|
||||
]);
|
||||
if (result.exitCode != 0) {
|
||||
throw StateError(
|
||||
'Failed to disable immersive mode confirmations: ${result.stderr}',
|
||||
);
|
||||
throw StateError('Failed to disable immersive mode confirmations: ${result.stderr}');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,9 +223,7 @@ sealed class AndroidDeviceTarget {
|
||||
/// ```dart
|
||||
/// const AndroidDeviceTarget target = AndroidDeviceTarget.bySerial('emulator-5554');
|
||||
/// ```
|
||||
const factory AndroidDeviceTarget.bySerial(
|
||||
String serialNumber,
|
||||
) = _SerialDeviceTarget;
|
||||
const factory AndroidDeviceTarget.bySerial(String serialNumber) = _SerialDeviceTarget;
|
||||
|
||||
/// Represents the only running emulator _or_ connected device.
|
||||
///
|
||||
@@ -314,5 +285,5 @@ enum AdbUserRotation {
|
||||
reversePortrait,
|
||||
|
||||
/// Reverse landscape orientation, i.e., landscape upside down.
|
||||
reverseLandscape;
|
||||
reverseLandscape,
|
||||
}
|
||||
|
||||
@@ -44,10 +44,7 @@ final class AndroidNativeDriver implements NativeDriver {
|
||||
String? adbPath,
|
||||
io.Directory? tempDirectory,
|
||||
}) async {
|
||||
final Adb adb = await Adb.create(
|
||||
adbPath: adbPath,
|
||||
target: target,
|
||||
);
|
||||
final Adb adb = await Adb.create(adbPath: adbPath, target: target);
|
||||
tempDirectory ??= io.Directory.systemTemp.createTempSync('native_driver.');
|
||||
final AndroidNativeDriver nativeDriver = AndroidNativeDriver.forTesting(
|
||||
adb: adb,
|
||||
|
||||
@@ -19,19 +19,13 @@ final class NativeCommand extends Command {
|
||||
}
|
||||
|
||||
/// Requests that the device be rotated to landscape mode.
|
||||
static const NativeCommand rotateLandscape = NativeCommand(
|
||||
'rotate_landscape',
|
||||
);
|
||||
static const NativeCommand rotateLandscape = NativeCommand('rotate_landscape');
|
||||
|
||||
/// Requests that the device reset its rotation to the default orientation.
|
||||
static const NativeCommand rotateDefault = NativeCommand(
|
||||
'rotate_default',
|
||||
);
|
||||
static const NativeCommand rotateDefault = NativeCommand('rotate_default');
|
||||
|
||||
/// Pings the device to ensure it is responsive.
|
||||
static const NativeCommand ping = NativeCommand(
|
||||
'ping',
|
||||
);
|
||||
static const NativeCommand ping = NativeCommand('ping');
|
||||
|
||||
/// The method to call on the plugin.
|
||||
final String method;
|
||||
@@ -88,10 +82,7 @@ final class ByNativeAccessibilityLabel extends NativeFinder {
|
||||
|
||||
@override
|
||||
Map<String, String> toJson() {
|
||||
return <String, String>{
|
||||
'kind': 'byNativeAccessibilityLabel',
|
||||
'label': label,
|
||||
};
|
||||
return <String, String>{'kind': 'byNativeAccessibilityLabel', 'label': label};
|
||||
}
|
||||
|
||||
/// Deserializes this finder from the value generated by [serialize].
|
||||
@@ -118,10 +109,7 @@ final class ByNativeIntegerId extends NativeFinder {
|
||||
|
||||
@override
|
||||
Map<String, String> toJson() {
|
||||
return <String, String>{
|
||||
'kind': 'byNativeIntegerId',
|
||||
'id': id.toString(),
|
||||
};
|
||||
return <String, String>{'kind': 'byNativeIntegerId', 'id': id.toString()};
|
||||
}
|
||||
|
||||
/// Deserializes this finder from the value generated by [serialize].
|
||||
|
||||
@@ -171,8 +171,7 @@ AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
|
||||
return switch (key) {
|
||||
Uri() => _MatchesGoldenFile(key, version),
|
||||
String() => _MatchesGoldenFile.forStringPath(key, version),
|
||||
_ => throw ArgumentError(
|
||||
'Unexpected type for golden file: ${key.runtimeType}'),
|
||||
_ => throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,8 +181,7 @@ final class _MatchesGoldenFile extends AsyncMatcher {
|
||||
const _MatchesGoldenFile(this.key, this.version);
|
||||
|
||||
/// Creates an instance of [MatchesGoldenFile] from a [String] path.
|
||||
_MatchesGoldenFile.forStringPath(String path, this.version)
|
||||
: key = Uri.parse(path);
|
||||
_MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);
|
||||
|
||||
/// The [key] to the golden image.
|
||||
final Uri key;
|
||||
@@ -201,9 +199,7 @@ final class _MatchesGoldenFile extends AsyncMatcher {
|
||||
} else if (item is FutureOr<NativeScreenshot>) {
|
||||
buffer = await (await item).readAsBytes();
|
||||
} else {
|
||||
throw ArgumentError(
|
||||
'Unexpected type for golden file: ${item.runtimeType}',
|
||||
);
|
||||
throw ArgumentError('Unexpected type for golden file: ${item.runtimeType}');
|
||||
}
|
||||
|
||||
if (autoUpdateGoldenFiles) {
|
||||
@@ -211,10 +207,7 @@ final class _MatchesGoldenFile extends AsyncMatcher {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final bool success = await goldenFileComparator.compare(
|
||||
buffer,
|
||||
testNameUri,
|
||||
);
|
||||
final bool success = await goldenFileComparator.compare(buffer, testNameUri);
|
||||
return success ? null : 'does not match';
|
||||
} on TestFailure catch (e) {
|
||||
return e.message;
|
||||
|
||||
@@ -18,10 +18,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
caughtArgs = <String>[];
|
||||
processManager = FakeProcessManager((
|
||||
String exec,
|
||||
List<String> args,
|
||||
) async {
|
||||
processManager = FakeProcessManager((String exec, List<String> args) async {
|
||||
assert(caughtArgs.isEmpty, 'Should only catch one call to run');
|
||||
caughtArgs = args;
|
||||
return FakeProcessManager.ok();
|
||||
@@ -74,14 +71,14 @@ void main() {
|
||||
});
|
||||
|
||||
expect(
|
||||
Adb.create(
|
||||
processManager: processManager,
|
||||
Adb.create(processManager: processManager),
|
||||
throwsA(
|
||||
isA<StateError>().having(
|
||||
(StateError e) => e.message,
|
||||
'message',
|
||||
'No device connected: error',
|
||||
),
|
||||
),
|
||||
throwsA(isA<StateError>().having(
|
||||
(StateError e) => e.message,
|
||||
'message',
|
||||
'No device connected: error',
|
||||
)),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -100,9 +97,7 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
final Adb adb = await Adb.create(
|
||||
processManager: processManager,
|
||||
);
|
||||
final Adb adb = await Adb.create(processManager: processManager);
|
||||
|
||||
final Uint8List result = await adb.screencap();
|
||||
expect(result, <int>[0, 1, 2, 3]);
|
||||
@@ -123,9 +118,7 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
final Adb adb = await Adb.create(
|
||||
processManager: processManager,
|
||||
);
|
||||
final Adb adb = await Adb.create(processManager: processManager);
|
||||
|
||||
await adb.tap(1, 2);
|
||||
});
|
||||
|
||||
@@ -13,9 +13,7 @@ import 'src/fake_driver.dart';
|
||||
|
||||
void main() async {
|
||||
test('screenshot calls ADB screencap', () async {
|
||||
final FakeAdb adb = FakeAdb(
|
||||
screencap: () async => Uint8List.fromList(<int>[1, 2, 3, 4]),
|
||||
);
|
||||
final FakeAdb adb = FakeAdb(screencap: () async => Uint8List.fromList(<int>[1, 2, 3, 4]));
|
||||
final io.Directory tmpDir = io.Directory.systemTemp.createTempSync();
|
||||
final AndroidNativeDriver driver = AndroidNativeDriver.forTesting(
|
||||
adb: adb,
|
||||
|
||||
@@ -19,14 +19,14 @@ final class FakeAdb implements Adb {
|
||||
Future<void> Function()? sendToHome,
|
||||
Future<void> Function(String appName, [String? activityName])? resumeApp,
|
||||
Future<void> Function(String appName)? trimMemory,
|
||||
}) : _isDeviceConnected = isDeviceConnected,
|
||||
_screencap = screencap,
|
||||
_tap = tap,
|
||||
_disableImmersiveModeConfirmations = disableImmersiveModeConfirmations,
|
||||
_disableAnimations = disableAnimations,
|
||||
_sendToHome = sendToHome,
|
||||
_resumeApp = resumeApp,
|
||||
_trimMemory = trimMemory;
|
||||
}) : _isDeviceConnected = isDeviceConnected,
|
||||
_screencap = screencap,
|
||||
_tap = tap,
|
||||
_disableImmersiveModeConfirmations = disableImmersiveModeConfirmations,
|
||||
_disableAnimations = disableAnimations,
|
||||
_sendToHome = sendToHome,
|
||||
_resumeApp = resumeApp,
|
||||
_trimMemory = trimMemory;
|
||||
|
||||
@override
|
||||
Future<(bool, String?)> isDeviceConnected() async {
|
||||
@@ -75,10 +75,7 @@ final class FakeAdb implements Adb {
|
||||
return _resumeApp?.call(appName, activityName) ?? Future<void>.value();
|
||||
}
|
||||
|
||||
final Future<void> Function(
|
||||
String appName, [
|
||||
String? activityName,
|
||||
])? _resumeApp;
|
||||
final Future<void> Function(String appName, [String? activityName])? _resumeApp;
|
||||
|
||||
@override
|
||||
Future<void> trimMemory({required String appName}) {
|
||||
|
||||
@@ -13,10 +13,7 @@ import 'package:process/process.dart';
|
||||
final class FakeProcessManager extends ProcessManager {
|
||||
/// Creates a new [FakeProcessManager] with the given delegate callback.
|
||||
FakeProcessManager(this._runDelegate);
|
||||
final Future<ProcessResult> Function(
|
||||
String executable,
|
||||
List<String> arguments,
|
||||
) _runDelegate;
|
||||
final Future<ProcessResult> Function(String executable, List<String> arguments) _runDelegate;
|
||||
|
||||
@override
|
||||
Never noSuchMethod(_) => throw UnimplementedError();
|
||||
|
||||
@@ -36,85 +36,75 @@ void main() {
|
||||
});
|
||||
|
||||
test('getBranchName does not call git if env LUCI_BRANCH provided', () {
|
||||
setUpWithEnvironment(
|
||||
<String, String>{
|
||||
'LUCI_BRANCH': branchName,
|
||||
},
|
||||
setUpWithEnvironment(<String, String>{'LUCI_BRANCH': branchName});
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'rev-parse', 'HEAD'],
|
||||
));
|
||||
expect(
|
||||
apidocs.FlutterInformation.instance.getBranchName(),
|
||||
branchName,
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
|
||||
);
|
||||
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
});
|
||||
|
||||
test('getBranchName calls git if env LUCI_BRANCH not provided', () {
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'rev-parse', 'HEAD'],
|
||||
));
|
||||
|
||||
expect(
|
||||
apidocs.FlutterInformation.instance.getBranchName(),
|
||||
branchName,
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
|
||||
);
|
||||
|
||||
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
});
|
||||
|
||||
test('getBranchName calls git if env LUCI_BRANCH is empty', () {
|
||||
setUpWithEnvironment(
|
||||
<String, String>{
|
||||
'LUCI_BRANCH': '',
|
||||
},
|
||||
setUpWithEnvironment(<String, String>{'LUCI_BRANCH': ''});
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
|
||||
);
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'rev-parse', 'HEAD'],
|
||||
));
|
||||
|
||||
expect(
|
||||
apidocs.FlutterInformation.instance.getBranchName(),
|
||||
branchName,
|
||||
);
|
||||
expect(apidocs.FlutterInformation.instance.getBranchName(), branchName);
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
});
|
||||
|
||||
test("runPubProcess doesn't use the pub binary", () {
|
||||
final Platform platform = FakePlatform(
|
||||
environment: <String, String>{
|
||||
'FLUTTER_ROOT': '/flutter',
|
||||
},
|
||||
environment: <String, String>{'FLUTTER_ROOT': '/flutter'},
|
||||
);
|
||||
final ProcessManager processManager = FakeProcessManager.list(
|
||||
<FakeCommand>[
|
||||
const FakeCommand(
|
||||
command: <String>['/flutter/bin/flutter', 'pub', '--one', '--two'],
|
||||
),
|
||||
],
|
||||
final ProcessManager processManager = FakeProcessManager.list(<FakeCommand>[
|
||||
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', '--one', '--two']),
|
||||
]);
|
||||
apidocs.FlutterInformation.instance = apidocs.FlutterInformation(
|
||||
platform: platform,
|
||||
processManager: processManager,
|
||||
filesystem: memoryFileSystem,
|
||||
);
|
||||
apidocs.FlutterInformation.instance =
|
||||
apidocs.FlutterInformation(platform: platform, processManager: processManager, filesystem: memoryFileSystem);
|
||||
|
||||
apidocs.runPubProcess(
|
||||
arguments: <String>['--one', '--two'],
|
||||
@@ -126,48 +116,56 @@ void main() {
|
||||
});
|
||||
|
||||
test('calls out to flutter if FLUTTER_VERSION is not set', () async {
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'rev-parse', 'HEAD'],
|
||||
));
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
|
||||
);
|
||||
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
|
||||
});
|
||||
test("doesn't call out to flutter if FLUTTER_VERSION is set", () async {
|
||||
setUpWithEnvironment(<String, String>{
|
||||
'FLUTTER_VERSION': testVersionInfo,
|
||||
});
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'rev-parse', 'HEAD'],
|
||||
));
|
||||
setUpWithEnvironment(<String, String>{'FLUTTER_VERSION': testVersionInfo});
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
|
||||
);
|
||||
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
expect(info['frameworkVersion'], equals(Version.parse('2.5.0')));
|
||||
});
|
||||
test('getFlutterRoot calls out to flutter if FLUTTER_ROOT is not set', () async {
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
));
|
||||
fakeProcessManager.addCommand(const FakeCommand(
|
||||
command: <Pattern>['git', 'rev-parse', 'HEAD'],
|
||||
));
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['flutter', '--version', '--machine'],
|
||||
stdout: testVersionInfo,
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
);
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(command: <Pattern>['git', 'rev-parse', 'HEAD']),
|
||||
);
|
||||
final Directory root = flutterInformation.getFlutterRoot();
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
expect(root.path, equals('/home/user/flutter'));
|
||||
@@ -185,9 +183,7 @@ void main() {
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['git', 'rev-parse', 'HEAD'],
|
||||
),
|
||||
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
|
||||
]);
|
||||
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
|
||||
expect(info['frameworkVersion'], isNotNull);
|
||||
@@ -215,9 +211,7 @@ void main() {
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['git', 'rev-parse', 'HEAD'],
|
||||
),
|
||||
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
|
||||
]);
|
||||
final Map<String, dynamic> info = flutterInformation.getFlutterInformation();
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
@@ -286,12 +280,8 @@ void main() {
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['git', 'rev-parse', 'HEAD'],
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list'],
|
||||
),
|
||||
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
|
||||
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
|
||||
FakeCommand(
|
||||
command: <Pattern>[
|
||||
'/flutter/bin/flutter',
|
||||
@@ -349,30 +339,51 @@ void main() {
|
||||
configurator.generateConfiguration();
|
||||
expect(packageRoot.childFile('pubspec.yaml').existsSync(), isTrue);
|
||||
expect(packageRoot.childFile('pubspec.yaml').readAsStringSync(), contains('flutter_gpu:'));
|
||||
expect(packageRoot.childFile('pubspec.yaml').readAsStringSync(), contains('dependency_overrides:'));
|
||||
expect(packageRoot.childFile('pubspec.yaml').readAsStringSync(), contains('platform_integration:'));
|
||||
expect(
|
||||
packageRoot.childFile('pubspec.yaml').readAsStringSync(),
|
||||
contains('dependency_overrides:'),
|
||||
);
|
||||
expect(
|
||||
packageRoot.childFile('pubspec.yaml').readAsStringSync(),
|
||||
contains('platform_integration:'),
|
||||
);
|
||||
});
|
||||
|
||||
test('.generateConfiguration generates fake lib', () async {
|
||||
configurator.generateConfiguration();
|
||||
expect(packageRoot.childDirectory('lib').existsSync(), isTrue);
|
||||
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').existsSync(), isTrue);
|
||||
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(), contains('library temp_doc;'));
|
||||
expect(packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(), contains("import 'package:flutter_gpu/gpu.dart';"));
|
||||
expect(
|
||||
packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(),
|
||||
contains('library temp_doc;'),
|
||||
);
|
||||
expect(
|
||||
packageRoot.childDirectory('lib').childFile('temp_doc.dart').readAsStringSync(),
|
||||
contains("import 'package:flutter_gpu/gpu.dart';"),
|
||||
);
|
||||
});
|
||||
|
||||
test('.generateConfiguration generates page footer', () async {
|
||||
configurator.generateConfiguration();
|
||||
expect(packageRoot.childFile('footer.html').existsSync(), isTrue);
|
||||
expect(packageRoot.childFile('footer.html').readAsStringSync(), contains('<script src="footer.js">'));
|
||||
expect(
|
||||
packageRoot.childFile('footer.html').readAsStringSync(),
|
||||
contains('<script src="footer.js">'),
|
||||
);
|
||||
expect(publishRoot.childDirectory('flutter').childFile('footer.js').existsSync(), isTrue);
|
||||
expect(publishRoot.childDirectory('flutter').childFile('footer.js').readAsStringSync(), contains(RegExp(r'Flutter 2.5.0 •.*• stable')));
|
||||
expect(
|
||||
publishRoot.childDirectory('flutter').childFile('footer.js').readAsStringSync(),
|
||||
contains(RegExp(r'Flutter 2.5.0 •.*• stable')),
|
||||
);
|
||||
});
|
||||
|
||||
test('.generateConfiguration generates search metadata', () async {
|
||||
configurator.generateConfiguration();
|
||||
expect(publishRoot.childFile('opensearch.xml').existsSync(), isTrue);
|
||||
expect(publishRoot.childFile('opensearch.xml').readAsStringSync(), contains('https://api.flutter.dev/'));
|
||||
expect(
|
||||
publishRoot.childFile('opensearch.xml').readAsStringSync(),
|
||||
contains('https://api.flutter.dev/'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -398,9 +409,7 @@ void main() {
|
||||
apidocs.FlutterInformation.instance = apidocs.FlutterInformation(
|
||||
filesystem: fs,
|
||||
processManager: processManager,
|
||||
platform: FakePlatform(environment: <String, String>{
|
||||
'FLUTTER_ROOT': repoRoot.path,
|
||||
}),
|
||||
platform: FakePlatform(environment: <String, String>{'FLUTTER_ROOT': repoRoot.path}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -415,12 +424,8 @@ void main() {
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['git', 'rev-parse', 'HEAD'],
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list'],
|
||||
),
|
||||
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
|
||||
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
|
||||
FakeCommand(
|
||||
command: <Pattern>[
|
||||
'/flutter/bin/flutter',
|
||||
@@ -480,7 +485,11 @@ void main() {
|
||||
isA<Exception>().having(
|
||||
(Exception e) => e.toString(),
|
||||
'message',
|
||||
contains(RegExp(r'Missing .* which probably means the documentation failed to build correctly.')),
|
||||
contains(
|
||||
RegExp(
|
||||
r'Missing .* which probably means the documentation failed to build correctly.',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -499,12 +508,8 @@ void main() {
|
||||
command: <Pattern>['git', 'status', '-b', '--porcelain'],
|
||||
stdout: '## $branchName',
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['git', 'rev-parse', 'HEAD'],
|
||||
),
|
||||
const FakeCommand(
|
||||
command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list'],
|
||||
),
|
||||
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD']),
|
||||
const FakeCommand(command: <String>['/flutter/bin/flutter', 'pub', 'global', 'list']),
|
||||
FakeCommand(
|
||||
command: <Pattern>[
|
||||
'/flutter/bin/flutter',
|
||||
@@ -562,28 +567,26 @@ void main() {
|
||||
publishRoot.childDirectory('flutter').childFile(path).createSync(recursive: true);
|
||||
}
|
||||
for (final String path in dartdocDirectiveCanaryLibraries) {
|
||||
publishRoot.childDirectory('flutter').childDirectory(path).createSync(recursive: true);
|
||||
publishRoot
|
||||
.childDirectory('flutter')
|
||||
.childDirectory(path)
|
||||
.createSync(recursive: true);
|
||||
}
|
||||
publishRoot.childDirectory('flutter').childFile('index.html').createSync();
|
||||
|
||||
final Directory widgetsDir = publishRoot
|
||||
.childDirectory('flutter')
|
||||
.childDirectory('widgets')
|
||||
..createSync(recursive: true);
|
||||
.childDirectory('flutter')
|
||||
.childDirectory('widgets')..createSync(recursive: true);
|
||||
widgetsDir.childFile('showGeneralDialog.html').writeAsStringSync('''
|
||||
<pre id="longSnippet1">
|
||||
<code class="language-dart">
|
||||
import 'package:flutter/material.dart';
|
||||
</code>
|
||||
</pre>
|
||||
''',
|
||||
);
|
||||
''');
|
||||
expect(publishRoot.childDirectory('flutter').existsSync(), isTrue);
|
||||
(widgetsDir
|
||||
.childDirectory('ModalRoute')
|
||||
..createSync(recursive: true))
|
||||
.childFile('barrierColor.html')
|
||||
.writeAsStringSync('''
|
||||
(widgetsDir.childDirectory('ModalRoute')
|
||||
..createSync(recursive: true)).childFile('barrierColor.html').writeAsStringSync('''
|
||||
<pre id="sample-code">
|
||||
<code class="language-dart">
|
||||
class FooClass {
|
||||
@@ -592,12 +595,13 @@ void main() {
|
||||
</code>
|
||||
</pre>
|
||||
''');
|
||||
const String queryParams = 'split=1&run=true&sample_id=widgets.Listener.123&channel=main';
|
||||
const String queryParams =
|
||||
'split=1&run=true&sample_id=widgets.Listener.123&channel=main';
|
||||
widgetsDir.childFile('Listener-class.html').writeAsStringSync('''
|
||||
<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?$queryParams">
|
||||
</iframe>
|
||||
''');
|
||||
}
|
||||
},
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -11,25 +11,26 @@ import '../bin/engine_hash.dart' show GitRevisionStrategy, engineHash;
|
||||
|
||||
void main() {
|
||||
test('Produces an engine hash for merge-base', () async {
|
||||
final Future<io.ProcessResult> Function(List<String>) runProcess =
|
||||
_fakeProcesses(processes: <FakeProcess>[
|
||||
(
|
||||
exe: 'git',
|
||||
command: 'merge-base',
|
||||
rest: <String>['upstream/master', 'HEAD'],
|
||||
exitCode: 0,
|
||||
stdout: 'abcdef1234',
|
||||
stderr: null
|
||||
),
|
||||
(
|
||||
exe: 'git',
|
||||
command: 'ls-tree',
|
||||
rest: <String>['-r', 'abcdef1234', 'engine', 'DEPS'],
|
||||
exitCode: 0,
|
||||
stdout: 'one\r\ntwo\r\n',
|
||||
stderr: null
|
||||
),
|
||||
]);
|
||||
final Future<io.ProcessResult> Function(List<String>) runProcess = _fakeProcesses(
|
||||
processes: <FakeProcess>[
|
||||
(
|
||||
exe: 'git',
|
||||
command: 'merge-base',
|
||||
rest: <String>['upstream/master', 'HEAD'],
|
||||
exitCode: 0,
|
||||
stdout: 'abcdef1234',
|
||||
stderr: null,
|
||||
),
|
||||
(
|
||||
exe: 'git',
|
||||
command: 'ls-tree',
|
||||
rest: <String>['-r', 'abcdef1234', 'engine', 'DEPS'],
|
||||
exitCode: 0,
|
||||
stdout: 'one\r\ntwo\r\n',
|
||||
stderr: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Future<String> result = engineHash(runProcess);
|
||||
|
||||
@@ -37,8 +38,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('Produces an engine hash for HEAD', () async {
|
||||
final Future<io.ProcessResult> Function(List<String>) runProcess =
|
||||
_fakeProcesses(
|
||||
final Future<io.ProcessResult> Function(List<String>) runProcess = _fakeProcesses(
|
||||
processes: <FakeProcess>[
|
||||
(
|
||||
exe: 'git',
|
||||
@@ -46,57 +46,54 @@ void main() {
|
||||
rest: <String>['-r', 'HEAD', 'engine', 'DEPS'],
|
||||
exitCode: 0,
|
||||
stdout: 'one\ntwo\n',
|
||||
stderr: null
|
||||
stderr: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Future<String> result =
|
||||
engineHash(runProcess, revisionStrategy: GitRevisionStrategy.head);
|
||||
final Future<String> result = engineHash(
|
||||
runProcess,
|
||||
revisionStrategy: GitRevisionStrategy.head,
|
||||
);
|
||||
|
||||
expect(result, completion('c708d7ef841f7e1748436b8ef5670d0b2de1a227'));
|
||||
});
|
||||
|
||||
test('Returns error in non-monorepo', () async {
|
||||
final Future<io.ProcessResult> Function(List<String>) runProcess =
|
||||
_fakeProcesses(processes: <FakeProcess>[
|
||||
(
|
||||
exe: 'git',
|
||||
command: 'ls-tree',
|
||||
rest: <String>['-r', 'HEAD', 'engine', 'DEPS'],
|
||||
exitCode: 0,
|
||||
stdout: '',
|
||||
stderr: null
|
||||
),
|
||||
]);
|
||||
final Future<io.ProcessResult> Function(List<String>) runProcess = _fakeProcesses(
|
||||
processes: <FakeProcess>[
|
||||
(
|
||||
exe: 'git',
|
||||
command: 'ls-tree',
|
||||
rest: <String>['-r', 'HEAD', 'engine', 'DEPS'],
|
||||
exitCode: 0,
|
||||
stdout: '',
|
||||
stderr: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Future<String> result =
|
||||
engineHash(runProcess, revisionStrategy: GitRevisionStrategy.head);
|
||||
final Future<String> result = engineHash(
|
||||
runProcess,
|
||||
revisionStrategy: GitRevisionStrategy.head,
|
||||
);
|
||||
|
||||
expect(result, throwsA('Not in a monorepo'));
|
||||
});
|
||||
}
|
||||
|
||||
typedef FakeProcess = ({
|
||||
String exe,
|
||||
String command,
|
||||
List<String> rest,
|
||||
dynamic stdout,
|
||||
dynamic stderr,
|
||||
int exitCode
|
||||
});
|
||||
typedef FakeProcess =
|
||||
({String exe, String command, List<String> rest, dynamic stdout, dynamic stderr, int exitCode});
|
||||
|
||||
Future<io.ProcessResult> Function(List<String>) _fakeProcesses({
|
||||
required List<FakeProcess> processes,
|
||||
}) =>
|
||||
(List<String> cmd) async {
|
||||
for (final FakeProcess process in processes) {
|
||||
if (process.exe.endsWith(cmd[0]) &&
|
||||
process.command.endsWith(cmd[1]) &&
|
||||
process.rest.equals(cmd.sublist(2))) {
|
||||
return io.ProcessResult(
|
||||
1, process.exitCode, process.stdout, process.stderr);
|
||||
}
|
||||
}
|
||||
return io.ProcessResult(1, -42, '', '404 command not found: $cmd');
|
||||
};
|
||||
}) => (List<String> cmd) async {
|
||||
for (final FakeProcess process in processes) {
|
||||
if (process.exe.endsWith(cmd[0]) &&
|
||||
process.command.endsWith(cmd[1]) &&
|
||||
process.rest.equals(cmd.sublist(2))) {
|
||||
return io.ProcessResult(1, process.exitCode, process.stdout, process.stderr);
|
||||
}
|
||||
}
|
||||
return io.ProcessResult(1, -42, '', '404 command not found: $cmd');
|
||||
};
|
||||
|
||||
@@ -75,38 +75,47 @@ void main() {
|
||||
'format.${io.Platform.isWindows ? 'bat' : 'sh'}',
|
||||
);
|
||||
|
||||
test('Can fix Dart formatting errors', () {
|
||||
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[
|
||||
dartContentPair,
|
||||
], flutterRoot);
|
||||
try {
|
||||
fixture.gitAdd();
|
||||
io.Process.runSync(formatterPath, <String>['--fix'], workingDirectory: flutterRoot.path);
|
||||
test(
|
||||
'Can fix Dart formatting errors',
|
||||
() {
|
||||
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[
|
||||
dartContentPair,
|
||||
], flutterRoot);
|
||||
try {
|
||||
fixture.gitAdd();
|
||||
io.Process.runSync(formatterPath, <String>['--fix'], workingDirectory: flutterRoot.path);
|
||||
|
||||
final Iterable<FileContentPair> files = fixture.getFileContents();
|
||||
for (final FileContentPair pair in files) {
|
||||
expect(pair.original, equals(pair.formatted));
|
||||
final Iterable<FileContentPair> files = fixture.getFileContents();
|
||||
for (final FileContentPair pair in files) {
|
||||
expect(pair.original, equals(pair.formatted));
|
||||
}
|
||||
} finally {
|
||||
fixture.gitRemove();
|
||||
}
|
||||
} finally {
|
||||
fixture.gitRemove();
|
||||
}
|
||||
});
|
||||
},
|
||||
// TODO(goderbauer): Re-enable after the formatting changes have landed.
|
||||
skip: true,
|
||||
);
|
||||
|
||||
test('Prints error if dart formatter fails', () {
|
||||
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[], flutterRoot);
|
||||
final io.File dartFile = io.File('${flutterRoot.path}/format_test2.dart');
|
||||
dartFile.writeAsStringSync('P\n');
|
||||
fixture.files.add(dartFile);
|
||||
test(
|
||||
'Prints error if dart formatter fails',
|
||||
() {
|
||||
final TestFileFixture fixture = TestFileFixture(<FileContentPair>[], flutterRoot);
|
||||
final io.File dartFile = io.File('${flutterRoot.path}/format_test2.dart');
|
||||
dartFile.writeAsStringSync('P\n');
|
||||
fixture.files.add(dartFile);
|
||||
|
||||
try {
|
||||
fixture.gitAdd();
|
||||
final io.ProcessResult result = io.Process.runSync(formatterPath, <String>[
|
||||
'--fix',
|
||||
], workingDirectory: flutterRoot.path);
|
||||
expect(result.stdout, contains('format_test2.dart produced the following error'));
|
||||
expect(result.exitCode, isNot(0));
|
||||
} finally {
|
||||
fixture.gitRemove();
|
||||
}
|
||||
});
|
||||
try {
|
||||
fixture.gitAdd();
|
||||
final io.ProcessResult result = io.Process.runSync(formatterPath, <String>[
|
||||
'--fix',
|
||||
], workingDirectory: flutterRoot.path);
|
||||
expect(result.stdout, contains('format_test2.dart produced the following error'));
|
||||
expect(result.exitCode, isNot(0));
|
||||
} finally {
|
||||
fixture.gitRemove();
|
||||
}
|
||||
}, // TODO(goderbauer): Re-enable after the formatting changes have landed.
|
||||
skip: true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,20 +6,10 @@ import 'package:test/test.dart';
|
||||
|
||||
import '../update_icons.dart';
|
||||
|
||||
Map<String, String> codepointsA = <String, String>{
|
||||
'airplane': '111',
|
||||
'boat': '222',
|
||||
};
|
||||
Map<String, String> codepointsB = <String, String>{
|
||||
'airplane': '333',
|
||||
};
|
||||
Map<String, String> codepointsC = <String, String>{
|
||||
'airplane': '111',
|
||||
'train': '444',
|
||||
};
|
||||
Map<String, String> codepointsUnderscore = <String, String>{
|
||||
'airplane__123': '111',
|
||||
};
|
||||
Map<String, String> codepointsA = <String, String>{'airplane': '111', 'boat': '222'};
|
||||
Map<String, String> codepointsB = <String, String>{'airplane': '333'};
|
||||
Map<String, String> codepointsC = <String, String>{'airplane': '111', 'train': '444'};
|
||||
Map<String, String> codepointsUnderscore = <String, String>{'airplane__123': '111'};
|
||||
|
||||
void main() {
|
||||
group('safety checks', () {
|
||||
@@ -45,17 +35,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('usage string is correct', () {
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('abc', '')).usage,
|
||||
'Icon(Icons.abc),',
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('abc', '')).usage, 'Icon(Icons.abc),');
|
||||
});
|
||||
|
||||
test('usage string is correct with replacement', () {
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('123', '')).usage,
|
||||
'Icon(Icons.onetwothree),',
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('123', '')).usage, 'Icon(Icons.onetwothree),');
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('123_rounded', '')).usage,
|
||||
'Icon(Icons.onetwothree_rounded),',
|
||||
@@ -64,58 +48,22 @@ void main() {
|
||||
|
||||
test('certain icons should be mirrored in RTL', () {
|
||||
// Exact match
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('help', '')).isMirroredInRTL, true);
|
||||
// Variant
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_rounded', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('help_rounded', '')).isMirroredInRTL, true);
|
||||
// Common suffixes
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_alt', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_new', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_off', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_on', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('help_alt', '')).isMirroredInRTL, true);
|
||||
expect(Icon(const MapEntry<String, String>('help_new', '')).isMirroredInRTL, true);
|
||||
expect(Icon(const MapEntry<String, String>('help_off', '')).isMirroredInRTL, true);
|
||||
expect(Icon(const MapEntry<String, String>('help_on', '')).isMirroredInRTL, true);
|
||||
// Common suffixes + variant
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_alt_rounded', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_new_rounded', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_off_rounded', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_on_rounded', '')).isMirroredInRTL,
|
||||
true,
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('help_alt_rounded', '')).isMirroredInRTL, true);
|
||||
expect(Icon(const MapEntry<String, String>('help_new_rounded', '')).isMirroredInRTL, true);
|
||||
expect(Icon(const MapEntry<String, String>('help_off_rounded', '')).isMirroredInRTL, true);
|
||||
expect(Icon(const MapEntry<String, String>('help_on_rounded', '')).isMirroredInRTL, true);
|
||||
// No match
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('help_center_rounded', '')).isMirroredInRTL,
|
||||
false,
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('help_center_rounded', '')).isMirroredInRTL, false);
|
||||
// No match
|
||||
expect(
|
||||
Icon(const MapEntry<String, String>('arrow', '')).isMirroredInRTL,
|
||||
false,
|
||||
);
|
||||
expect(Icon(const MapEntry<String, String>('arrow', '')).isMirroredInRTL, false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,11 +29,7 @@ const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart';
|
||||
const String _defaultNewCodepointsPath = 'codepoints';
|
||||
const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints';
|
||||
const String _defaultFontFamily = 'MaterialIcons';
|
||||
const List<String> _defaultPossibleStyleSuffixes = <String>[
|
||||
'_outlined',
|
||||
'_rounded',
|
||||
'_sharp',
|
||||
];
|
||||
const List<String> _defaultPossibleStyleSuffixes = <String>['_outlined', '_rounded', '_sharp'];
|
||||
const String _defaultClassName = 'Icons';
|
||||
const String _defaultDemoFilePath = '/tmp/new_icons_demo.dart';
|
||||
|
||||
@@ -181,12 +177,16 @@ void main(List<String> args) {
|
||||
|
||||
final ArgResults argResults = _handleArguments(args);
|
||||
|
||||
final File iconsFile = File(path.normalize(path.absolute(argResults[_iconsPathOption] as String)));
|
||||
final File iconsFile = File(
|
||||
path.normalize(path.absolute(argResults[_iconsPathOption] as String)),
|
||||
);
|
||||
if (!iconsFile.existsSync()) {
|
||||
stderr.writeln('Error: Icons file not found: ${iconsFile.path}');
|
||||
exit(1);
|
||||
}
|
||||
final File iconsTemplateFile = File(path.normalize(path.absolute(argResults[_iconsTemplatePathOption] as String)));
|
||||
final File iconsTemplateFile = File(
|
||||
path.normalize(path.absolute(argResults[_iconsTemplatePathOption] as String)),
|
||||
);
|
||||
if (!iconsTemplateFile.existsSync()) {
|
||||
stderr.writeln('Error: Icons template file not found: ${iconsTemplateFile.path}');
|
||||
exit(1);
|
||||
@@ -216,7 +216,9 @@ void main(List<String> args) {
|
||||
}
|
||||
final String iconsTemplateContents = iconsTemplateFile.readAsStringSync();
|
||||
|
||||
stderr.writeln("Generating icons ${argResults[_dryRunOption] as bool ? '' : 'to ${iconsFile.path}'}");
|
||||
stderr.writeln(
|
||||
"Generating icons ${argResults[_dryRunOption] as bool ? '' : 'to ${iconsFile.path}'}",
|
||||
);
|
||||
final String newIconsContents = _regenerateIconsFile(
|
||||
iconsTemplateContents,
|
||||
newTokenPairMap,
|
||||
@@ -230,56 +232,83 @@ void main(List<String> args) {
|
||||
} else {
|
||||
iconsFile.writeAsStringSync(newIconsContents);
|
||||
|
||||
final SplayTreeMap<String, String> sortedNewTokenPairMap = SplayTreeMap<String, String>.of(newTokenPairMap);
|
||||
final SplayTreeMap<String, String> sortedNewTokenPairMap = SplayTreeMap<String, String>.of(
|
||||
newTokenPairMap,
|
||||
);
|
||||
_regenerateCodepointsFile(oldCodepointsFile, sortedNewTokenPairMap);
|
||||
|
||||
sortedNewTokenPairMap.removeWhere((String key, String value) => oldTokenPairMap.containsKey(key));
|
||||
sortedNewTokenPairMap.removeWhere(
|
||||
(String key, String value) => oldTokenPairMap.containsKey(key),
|
||||
);
|
||||
_generateIconDemo(File(_defaultDemoFilePath), sortedNewTokenPairMap);
|
||||
}
|
||||
}
|
||||
|
||||
ArgResults _handleArguments(List<String> args) {
|
||||
final ArgParser argParser = ArgParser()
|
||||
..addOption(_iconsPathOption,
|
||||
defaultsTo: _defaultIconsPath,
|
||||
help: 'Location of the material icons file')
|
||||
..addOption(_iconsTemplatePathOption,
|
||||
defaultsTo: _defaultIconsPath,
|
||||
help:
|
||||
'Location of the material icons file template. Usually the same as --$_iconsPathOption')
|
||||
..addOption(_newCodepointsPathOption,
|
||||
defaultsTo: _defaultNewCodepointsPath,
|
||||
help: 'Location of the new codepoints directory')
|
||||
..addOption(_oldCodepointsPathOption,
|
||||
defaultsTo: _defaultOldCodepointsPath,
|
||||
help: 'Location of the existing codepoints directory')
|
||||
..addOption(_fontFamilyOption,
|
||||
defaultsTo: _defaultFontFamily,
|
||||
help: 'The font family to use for the IconData constants')
|
||||
..addMultiOption(_possibleStyleSuffixesOption,
|
||||
defaultsTo: _defaultPossibleStyleSuffixes,
|
||||
help: 'A comma-separated list of suffixes (typically an optional '
|
||||
'family + a style) e.g. _outlined, _monoline_filled')
|
||||
..addOption(_classNameOption,
|
||||
defaultsTo: _defaultClassName,
|
||||
help: 'The containing class for all icons')
|
||||
..addFlag(_enforceSafetyChecks,
|
||||
defaultsTo: true,
|
||||
help: 'Whether to exit if safety checks fail (e.g. codepoints are missing or unstable')
|
||||
..addFlag(_dryRunOption);
|
||||
argParser.addFlag('help', abbr: 'h', negatable: false, callback: (bool help) {
|
||||
if (help) {
|
||||
print(argParser.usage);
|
||||
exit(1);
|
||||
}
|
||||
});
|
||||
final ArgParser argParser =
|
||||
ArgParser()
|
||||
..addOption(
|
||||
_iconsPathOption,
|
||||
defaultsTo: _defaultIconsPath,
|
||||
help: 'Location of the material icons file',
|
||||
)
|
||||
..addOption(
|
||||
_iconsTemplatePathOption,
|
||||
defaultsTo: _defaultIconsPath,
|
||||
help:
|
||||
'Location of the material icons file template. Usually the same as --$_iconsPathOption',
|
||||
)
|
||||
..addOption(
|
||||
_newCodepointsPathOption,
|
||||
defaultsTo: _defaultNewCodepointsPath,
|
||||
help: 'Location of the new codepoints directory',
|
||||
)
|
||||
..addOption(
|
||||
_oldCodepointsPathOption,
|
||||
defaultsTo: _defaultOldCodepointsPath,
|
||||
help: 'Location of the existing codepoints directory',
|
||||
)
|
||||
..addOption(
|
||||
_fontFamilyOption,
|
||||
defaultsTo: _defaultFontFamily,
|
||||
help: 'The font family to use for the IconData constants',
|
||||
)
|
||||
..addMultiOption(
|
||||
_possibleStyleSuffixesOption,
|
||||
defaultsTo: _defaultPossibleStyleSuffixes,
|
||||
help:
|
||||
'A comma-separated list of suffixes (typically an optional '
|
||||
'family + a style) e.g. _outlined, _monoline_filled',
|
||||
)
|
||||
..addOption(
|
||||
_classNameOption,
|
||||
defaultsTo: _defaultClassName,
|
||||
help: 'The containing class for all icons',
|
||||
)
|
||||
..addFlag(
|
||||
_enforceSafetyChecks,
|
||||
defaultsTo: true,
|
||||
help: 'Whether to exit if safety checks fail (e.g. codepoints are missing or unstable',
|
||||
)
|
||||
..addFlag(_dryRunOption);
|
||||
argParser.addFlag(
|
||||
'help',
|
||||
abbr: 'h',
|
||||
negatable: false,
|
||||
callback: (bool help) {
|
||||
if (help) {
|
||||
print(argParser.usage);
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
);
|
||||
return argParser.parse(args);
|
||||
}
|
||||
|
||||
Map<String, String> stringToTokenPairMap(String codepointData) {
|
||||
final Iterable<String> cleanData = LineSplitter.split(codepointData)
|
||||
.map((String line) => line.trim())
|
||||
.where((String line) => line.isNotEmpty);
|
||||
final Iterable<String> cleanData = LineSplitter.split(
|
||||
codepointData,
|
||||
).map((String line) => line.trim()).where((String line) => line.isNotEmpty);
|
||||
|
||||
final Map<String, String> pairs = <String, String>{};
|
||||
|
||||
@@ -295,16 +324,19 @@ Map<String, String> stringToTokenPairMap(String codepointData) {
|
||||
}
|
||||
|
||||
String _regenerateIconsFile(
|
||||
String templateFileContents,
|
||||
Map<String, String> tokenPairMap,
|
||||
String fontFamily,
|
||||
String className,
|
||||
bool enforceSafetyChecks,
|
||||
) {
|
||||
final List<Icon> newIcons = tokenPairMap.entries
|
||||
.map((MapEntry<String, String> entry) =>
|
||||
Icon(entry, fontFamily: fontFamily, className: className))
|
||||
.toList();
|
||||
String templateFileContents,
|
||||
Map<String, String> tokenPairMap,
|
||||
String fontFamily,
|
||||
String className,
|
||||
bool enforceSafetyChecks,
|
||||
) {
|
||||
final List<Icon> newIcons =
|
||||
tokenPairMap.entries
|
||||
.map(
|
||||
(MapEntry<String, String> entry) =>
|
||||
Icon(entry, fontFamily: fontFamily, className: className),
|
||||
)
|
||||
.toList();
|
||||
newIcons.sort((Icon a, Icon b) => a._compareTo(b));
|
||||
|
||||
final StringBuffer buf = StringBuffer();
|
||||
@@ -324,11 +356,13 @@ String _regenerateIconsFile(
|
||||
for (final String style in <String>['', '_outlined', '_rounded', '_sharp']) {
|
||||
try {
|
||||
final Icon agnosticIcon = newIcons.firstWhere(
|
||||
(Icon icon) => icon.id == '${ids[0]}$style',
|
||||
orElse: () => throw ids[0]);
|
||||
(Icon icon) => icon.id == '${ids[0]}$style',
|
||||
orElse: () => throw ids[0],
|
||||
);
|
||||
final Icon iOSIcon = newIcons.firstWhere(
|
||||
(Icon icon) => icon.id == '${ids[1]}$style',
|
||||
orElse: () => throw ids[1]);
|
||||
(Icon icon) => icon.id == '${ids[1]}$style',
|
||||
orElse: () => throw ids[1],
|
||||
);
|
||||
platformAdaptiveDeclarations.add(
|
||||
agnosticIcon.platformAdaptiveDeclaration('$flutterId$style', iOSIcon),
|
||||
);
|
||||
@@ -355,7 +389,8 @@ String _regenerateIconsFile(
|
||||
// Generate for Icons
|
||||
if (line.contains(_beginGeneratedMark)) {
|
||||
generating = true;
|
||||
final String iconDeclarationsString = newIcons.map((Icon icon) => icon.fullDeclaration).join();
|
||||
final String iconDeclarationsString =
|
||||
newIcons.map((Icon icon) => icon.fullDeclaration).join();
|
||||
buf.write(iconDeclarationsString);
|
||||
} else if (line.contains(_endGeneratedMark)) {
|
||||
generating = false;
|
||||
@@ -376,11 +411,14 @@ bool testIsSuperset(Map<String, String> newCodepoints, Map<String, String> oldCo
|
||||
}
|
||||
if (!newCodepointsSet.containsAll(oldCodepointsSet)) {
|
||||
stderr.writeln(
|
||||
'❌ new codepoints file does not contain all ${oldCodepointsSet.length} '
|
||||
'existing codepoints. Missing: ${oldCodepointsSet.difference(newCodepointsSet)}');
|
||||
'❌ new codepoints file does not contain all ${oldCodepointsSet.length} '
|
||||
'existing codepoints. Missing: ${oldCodepointsSet.difference(newCodepointsSet)}',
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
stderr.writeln('✅ new codepoints file contains all ${oldCodepointsSet.length} existing codepoints');
|
||||
stderr.writeln(
|
||||
'✅ new codepoints file contains all ${oldCodepointsSet.length} existing codepoints',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -394,7 +432,9 @@ bool testIsStable(Map<String, String> newCodepoints, Map<String, String> oldCode
|
||||
];
|
||||
|
||||
if (unstable.isNotEmpty) {
|
||||
stderr.writeln('❌ out of $oldCodepointsCount existing codepoints, ${unstable.length} were unstable: $unstable');
|
||||
stderr.writeln(
|
||||
'❌ out of $oldCodepointsCount existing codepoints, ${unstable.length} were unstable: $unstable',
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
stderr.writeln('✅ all existing $oldCodepointsCount codepoints are stable');
|
||||
@@ -449,7 +489,8 @@ void _generateIconDemo(File demoFilePath, Map<String, String> tokenPairMap) {
|
||||
|
||||
class Icon {
|
||||
// Parse tokenPair (e.g. {"6_ft_apart_outlined": "e004"}).
|
||||
Icon(MapEntry<String, String> tokenPair, {
|
||||
Icon(
|
||||
MapEntry<String, String> tokenPair, {
|
||||
this.fontFamily = _defaultFontFamily,
|
||||
this.possibleStyleSuffixes = _defaultPossibleStyleSuffixes,
|
||||
this.className = _defaultClassName,
|
||||
@@ -488,7 +529,6 @@ class Icon {
|
||||
_generateFlutterId();
|
||||
}
|
||||
|
||||
|
||||
late String id; // e.g. 5g, 5g_outlined, 5g_rounded, 5g_sharp
|
||||
late String shortId; // e.g. 5g
|
||||
late String flutterId; // e.g. five_g, five_g_outlined, five_g_rounded, five_g_sharp
|
||||
@@ -496,12 +536,14 @@ class Icon {
|
||||
late String dartdocFamily; // e.g. material
|
||||
late String dartdocHtmlSuffix = ''; // The suffix for the 'material-icons' HTML class.
|
||||
String fontFamily; // The IconData font family.
|
||||
List<String> possibleStyleSuffixes; // A list of possible suffixes e.g. _outlined, _monoline_filled.
|
||||
List<String>
|
||||
possibleStyleSuffixes; // A list of possible suffixes e.g. _outlined, _monoline_filled.
|
||||
String className; // The containing class.
|
||||
|
||||
String get name => shortId.replaceAll('_', ' ').trim();
|
||||
|
||||
String get style => dartdocHtmlSuffix == '' ? '' : ' (${dartdocHtmlSuffix.replaceFirst('-', '')})';
|
||||
String get style =>
|
||||
dartdocHtmlSuffix == '' ? '' : ' (${dartdocHtmlSuffix.replaceFirst('-', '')})';
|
||||
|
||||
String get dartDoc =>
|
||||
'<i class="material-icons$dartdocHtmlSuffix md-36">$shortId</i> — $dartdocFamily icon named "$name"$style';
|
||||
@@ -511,7 +553,9 @@ class Icon {
|
||||
bool get isMirroredInRTL {
|
||||
// Remove common suffixes (e.g. "_new" or "_alt") from the shortId.
|
||||
final String normalizedShortId = shortId.replaceAll(RegExp(r'_(new|alt|off|on)$'), '');
|
||||
return _iconsMirroredWhenRTL.any((String shortIdMirroredWhenRTL) => normalizedShortId == shortIdMirroredWhenRTL);
|
||||
return _iconsMirroredWhenRTL.any(
|
||||
(String shortIdMirroredWhenRTL) => normalizedShortId == shortIdMirroredWhenRTL,
|
||||
);
|
||||
}
|
||||
|
||||
String get declaration =>
|
||||
@@ -562,19 +606,13 @@ class Icon {
|
||||
// Exact identifier rewrites.
|
||||
for (final MapEntry<String, String> rewritePair in _identifierExactRewrites.entries) {
|
||||
if (shortId == rewritePair.key) {
|
||||
flutterId = id.replaceFirst(
|
||||
rewritePair.key,
|
||||
_identifierExactRewrites[rewritePair.key]!,
|
||||
);
|
||||
flutterId = id.replaceFirst(rewritePair.key, _identifierExactRewrites[rewritePair.key]!);
|
||||
}
|
||||
}
|
||||
// Prefix identifier rewrites.
|
||||
for (final MapEntry<String, String> rewritePair in _identifierPrefixRewrites.entries) {
|
||||
if (id.startsWith(rewritePair.key)) {
|
||||
flutterId = id.replaceFirst(
|
||||
rewritePair.key,
|
||||
_identifierPrefixRewrites[rewritePair.key]!,
|
||||
);
|
||||
flutterId = id.replaceFirst(rewritePair.key, _identifierPrefixRewrites[rewritePair.key]!);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,58 +8,48 @@ import 'package:args/args.dart';
|
||||
import 'package:vitool/vitool.dart';
|
||||
|
||||
const String kCodegenComment =
|
||||
'// AUTOGENERATED FILE DO NOT EDIT!\n'
|
||||
'// This file was generated by vitool.\n';
|
||||
'// AUTOGENERATED FILE DO NOT EDIT!\n'
|
||||
'// This file was generated by vitool.\n';
|
||||
|
||||
void main(List<String> args) {
|
||||
final ArgParser parser = ArgParser();
|
||||
|
||||
parser.addFlag(
|
||||
'help',
|
||||
abbr: 'h',
|
||||
negatable: false,
|
||||
help: "Display the tool's usage instructions and quit.",
|
||||
'help',
|
||||
abbr: 'h',
|
||||
negatable: false,
|
||||
help: "Display the tool's usage instructions and quit.",
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'output',
|
||||
abbr: 'o',
|
||||
help: 'Target path to write the generated Dart file to.',
|
||||
);
|
||||
parser.addOption('output', abbr: 'o', help: 'Target path to write the generated Dart file to.');
|
||||
|
||||
parser.addOption('asset-name', abbr: 'n', help: 'Name to be used for the generated constant.');
|
||||
|
||||
parser.addOption('part-of', abbr: 'p', help: "Library name to add a dart 'part of' clause for.");
|
||||
|
||||
parser.addOption(
|
||||
'asset-name',
|
||||
abbr: 'n',
|
||||
help: 'Name to be used for the generated constant.',
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'part-of',
|
||||
abbr: 'p',
|
||||
help: "Library name to add a dart 'part of' clause for.",
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'header',
|
||||
abbr: 'd',
|
||||
help: 'File whose contents are to be prepended to the beginning of '
|
||||
'the generated Dart file; this can be used for a license comment.',
|
||||
'header',
|
||||
abbr: 'd',
|
||||
help:
|
||||
'File whose contents are to be prepended to the beginning of '
|
||||
'the generated Dart file; this can be used for a license comment.',
|
||||
);
|
||||
|
||||
parser.addFlag(
|
||||
'codegen_comment',
|
||||
abbr: 'c',
|
||||
defaultsTo: true,
|
||||
help: 'Whether to include the following comment after the header:\n'
|
||||
'$kCodegenComment',
|
||||
'codegen_comment',
|
||||
abbr: 'c',
|
||||
defaultsTo: true,
|
||||
help:
|
||||
'Whether to include the following comment after the header:\n'
|
||||
'$kCodegenComment',
|
||||
);
|
||||
|
||||
final ArgResults argResults = parser.parse(args);
|
||||
|
||||
if (argResults['help'] as bool ||
|
||||
!argResults.wasParsed('output') ||
|
||||
!argResults.wasParsed('asset-name') ||
|
||||
argResults.rest.isEmpty) {
|
||||
!argResults.wasParsed('output') ||
|
||||
!argResults.wasParsed('asset-name') ||
|
||||
argResults.rest.isEmpty) {
|
||||
printUsage(parser);
|
||||
return;
|
||||
}
|
||||
@@ -80,7 +70,9 @@ void main(List<String> args) {
|
||||
}
|
||||
|
||||
if (argResults.wasParsed('part-of')) {
|
||||
generatedSb.write('part of ${argResults['part-of']}; // ignore: use_string_in_part_of_directives\n');
|
||||
generatedSb.write(
|
||||
'part of ${argResults['part-of']}; // ignore: use_string_in_part_of_directives\n',
|
||||
);
|
||||
}
|
||||
|
||||
final Animation animation = Animation.fromFrameData(frames);
|
||||
@@ -92,6 +84,8 @@ void main(List<String> args) {
|
||||
|
||||
void printUsage(ArgParser parser) {
|
||||
print('Usage: vitool --asset-name=<asset_name> --output=<output_path> <frames_list>');
|
||||
print('\nExample: vitool --asset-name=_\$menu_arrow --output=lib/data/menu_arrow.g.dart assets/svg/menu_arrow/*.svg\n');
|
||||
print(
|
||||
'\nExample: vitool --asset-name=_\$menu_arrow --output=lib/data/menu_arrow.g.dart assets/svg/menu_arrow/*.svg\n',
|
||||
);
|
||||
print(parser.usage);
|
||||
}
|
||||
|
||||
@@ -40,16 +40,16 @@ class Animation {
|
||||
final FrameData frame = frames[i];
|
||||
if (size != frame.size) {
|
||||
throw Exception(
|
||||
'All animation frames must have the same size,\n'
|
||||
'first frame size was: (${size.x}, ${size.y})\n'
|
||||
'frame $i size was: (${frame.size.x}, ${frame.size.y})'
|
||||
'All animation frames must have the same size,\n'
|
||||
'first frame size was: (${size.x}, ${size.y})\n'
|
||||
'frame $i size was: (${frame.size.x}, ${frame.size.y})',
|
||||
);
|
||||
}
|
||||
if (numPaths != frame.paths.length) {
|
||||
throw Exception(
|
||||
'All animation frames must have the same number of paths,\n'
|
||||
'first frame has $numPaths paths\n'
|
||||
'frame $i has ${frame.paths.length} paths'
|
||||
'All animation frames must have the same number of paths,\n'
|
||||
'first frame has $numPaths paths\n'
|
||||
'frame $i has ${frame.paths.length} paths',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,18 +79,25 @@ class PathAnimation {
|
||||
}
|
||||
|
||||
final List<PathCommandAnimation> commands = <PathCommandAnimation>[];
|
||||
for (int commandIdx = 0; commandIdx < frames[0].paths[pathIdx].commands.length; commandIdx += 1) {
|
||||
for (
|
||||
int commandIdx = 0;
|
||||
commandIdx < frames[0].paths[pathIdx].commands.length;
|
||||
commandIdx += 1
|
||||
) {
|
||||
final int numPointsInCommand = frames[0].paths[pathIdx].commands[commandIdx].points.length;
|
||||
final List<List<Point<double>>> points = List<List<Point<double>>>.filled(numPointsInCommand, <Point<double>>[]);
|
||||
final List<List<Point<double>>> points = List<List<Point<double>>>.filled(
|
||||
numPointsInCommand,
|
||||
<Point<double>>[],
|
||||
);
|
||||
final String commandType = frames[0].paths[pathIdx].commands[commandIdx].type;
|
||||
for (int i = 0; i < frames.length; i += 1) {
|
||||
final FrameData frame = frames[i];
|
||||
final String currentCommandType = frame.paths[pathIdx].commands[commandIdx].type;
|
||||
if (commandType != currentCommandType) {
|
||||
throw Exception(
|
||||
'Paths must be built from the same commands in all frames '
|
||||
"command $commandIdx at frame 0 was of type '$commandType' "
|
||||
"command $commandIdx at frame $i was of type '$currentCommandType'"
|
||||
'Paths must be built from the same commands in all frames '
|
||||
"command $commandIdx at frame 0 was of type '$commandType' "
|
||||
"command $commandIdx at frame $i was of type '$currentCommandType'",
|
||||
);
|
||||
}
|
||||
for (int j = 0; j < numPointsInCommand; j += 1) {
|
||||
@@ -101,13 +108,14 @@ class PathAnimation {
|
||||
}
|
||||
|
||||
final List<double> opacities =
|
||||
frames.map<double>((FrameData d) => d.paths[pathIdx].opacity).toList();
|
||||
frames.map<double>((FrameData d) => d.paths[pathIdx].opacity).toList();
|
||||
|
||||
return PathAnimation(commands, opacities: opacities);
|
||||
}
|
||||
|
||||
/// List of commands for drawing the path.
|
||||
final List<PathCommandAnimation> commands;
|
||||
|
||||
/// The path opacity for each animation frame.
|
||||
final List<double> opacities;
|
||||
|
||||
@@ -190,8 +198,7 @@ FrameData interpretSvg(String svgFilePath) {
|
||||
final double width = parsePixels(_extractAttr(svgElement, 'width')).toDouble();
|
||||
final double height = parsePixels(_extractAttr(svgElement, 'height')).toDouble();
|
||||
|
||||
final List<SvgPath> paths =
|
||||
_interpretSvgGroup(svgElement.children, _Transform());
|
||||
final List<SvgPath> paths = _interpretSvgGroup(svgElement.children, _Transform());
|
||||
return FrameData(Point<double>(width, height), paths);
|
||||
}
|
||||
|
||||
@@ -216,7 +223,8 @@ List<SvgPath> _interpretSvgGroup(List<XmlNode> children, _Transform transform) {
|
||||
Matrix3 transformMatrix = transform.transformMatrix;
|
||||
if (_hasAttr(element, 'transform')) {
|
||||
transformMatrix = transformMatrix.multiplied(
|
||||
_parseSvgTransform(_extractAttr(element, 'transform')));
|
||||
_parseSvgTransform(_extractAttr(element, 'transform')),
|
||||
);
|
||||
}
|
||||
|
||||
final _Transform subtreeTransform = _Transform(
|
||||
@@ -249,10 +257,7 @@ List<Point<double>> parsePoints(String points) {
|
||||
final List<Point<double>> result = <Point<double>>[];
|
||||
while (unParsed.isNotEmpty && _pointMatcher.hasMatch(unParsed)) {
|
||||
final Match m = _pointMatcher.firstMatch(unParsed)!;
|
||||
result.add(Point<double>(
|
||||
double.parse(m.group(1)!),
|
||||
double.parse(m.group(2)!),
|
||||
));
|
||||
result.add(Point<double>(double.parse(m.group(1)!), double.parse(m.group(2)!)));
|
||||
unParsed = m.group(3)!;
|
||||
}
|
||||
return result;
|
||||
@@ -271,9 +276,9 @@ class FrameData {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is FrameData
|
||||
&& other.size == size
|
||||
&& const ListEquality<SvgPath>().equals(other.paths, paths);
|
||||
return other is FrameData &&
|
||||
other.size == size &&
|
||||
const ListEquality<SvgPath>().equals(other.paths, paths);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -317,7 +322,7 @@ class SvgPath {
|
||||
|
||||
SvgPath _applyTransform(_Transform transform) {
|
||||
final List<SvgPathCommand> transformedCommands =
|
||||
commands.map<SvgPathCommand>((SvgPathCommand c) => c._applyTransform(transform)).toList();
|
||||
commands.map<SvgPathCommand>((SvgPathCommand c) => c._applyTransform(transform)).toList();
|
||||
return SvgPath(id, transformedCommands, opacity: opacity * transform.opacity);
|
||||
}
|
||||
|
||||
@@ -326,10 +331,10 @@ class SvgPath {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is SvgPath
|
||||
&& other.id == id
|
||||
&& other.opacity == opacity
|
||||
&& const ListEquality<SvgPathCommand>().equals(other.commands, commands);
|
||||
return other is SvgPath &&
|
||||
other.id == id &&
|
||||
other.opacity == opacity &&
|
||||
const ListEquality<SvgPathCommand>().equals(other.commands, commands);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -339,7 +344,6 @@ class SvgPath {
|
||||
String toString() {
|
||||
return 'SvgPath(id: $id, opacity: $opacity, commands: $commands)';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Represents a single SVG path command from an SVG d element.
|
||||
@@ -363,11 +367,8 @@ class SvgPathCommand {
|
||||
final List<Point<double>> points;
|
||||
|
||||
SvgPathCommand _applyTransform(_Transform transform) {
|
||||
final List<Point<double>> transformedPoints =
|
||||
_vector3ArrayToPoints(
|
||||
transform.transformMatrix.applyToVector3Array(
|
||||
_pointsToVector3Array(points)
|
||||
)
|
||||
final List<Point<double>> transformedPoints = _vector3ArrayToPoints(
|
||||
transform.transformMatrix.applyToVector3Array(_pointsToVector3Array(points)),
|
||||
);
|
||||
return SvgPathCommand(type, transformedPoints);
|
||||
}
|
||||
@@ -377,9 +378,9 @@ class SvgPathCommand {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is SvgPathCommand
|
||||
&& other.type == type
|
||||
&& const ListEquality<Point<double>>().equals(other.points, points);
|
||||
return other is SvgPathCommand &&
|
||||
other.type == type &&
|
||||
const ListEquality<Point<double>>().equals(other.points, points);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -392,7 +393,7 @@ class SvgPathCommand {
|
||||
}
|
||||
|
||||
class SvgPathCommandBuilder {
|
||||
static const Map<String, void> kRelativeCommands = <String, void> {
|
||||
static const Map<String, void> kRelativeCommands = <String, void>{
|
||||
'c': null,
|
||||
'l': null,
|
||||
'm': null,
|
||||
@@ -440,8 +441,7 @@ List<double> _pointsToVector3Array(List<Point<double>> points) {
|
||||
List<Point<double>> _vector3ArrayToPoints(List<double> vector) {
|
||||
final int numPoints = (vector.length / 3).floor();
|
||||
final List<Point<double>> points = <Point<double>>[
|
||||
for (int i = 0; i < numPoints; i += 1)
|
||||
Point<double>(vector[i*3], vector[i*3 + 1]),
|
||||
for (int i = 0; i < numPoints; i += 1) Point<double>(vector[i * 3], vector[i * 3 + 1]),
|
||||
];
|
||||
return points;
|
||||
}
|
||||
@@ -451,23 +451,21 @@ List<Point<double>> _vector3ArrayToPoints(List<double> vector) {
|
||||
/// This includes more transforms than the ones described by the SVG transform
|
||||
/// attribute, e.g opacity.
|
||||
class _Transform {
|
||||
|
||||
/// Constructs a new _Transform, default arguments create a no-op transform.
|
||||
_Transform({Matrix3? transformMatrix, this.opacity = 1.0}) :
|
||||
transformMatrix = transformMatrix ?? Matrix3.identity();
|
||||
_Transform({Matrix3? transformMatrix, this.opacity = 1.0})
|
||||
: transformMatrix = transformMatrix ?? Matrix3.identity();
|
||||
|
||||
final Matrix3 transformMatrix;
|
||||
final double opacity;
|
||||
|
||||
_Transform applyTransform(_Transform transform) {
|
||||
return _Transform(
|
||||
transformMatrix: transform.transformMatrix.multiplied(transformMatrix),
|
||||
opacity: transform.opacity * opacity,
|
||||
transformMatrix: transform.transformMatrix.multiplied(transformMatrix),
|
||||
opacity: transform.opacity * opacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const String _transformCommandAtom = r' *([^(]+)\(([^)]*)\)';
|
||||
final RegExp _transformValidator = RegExp('^($_transformCommandAtom)*\$');
|
||||
final RegExp _transformCommand = RegExp(_transformCommandAtom);
|
||||
@@ -476,7 +474,7 @@ Matrix3 _parseSvgTransform(String transform) {
|
||||
if (!_transformValidator.hasMatch(transform)) {
|
||||
throw Exception('illegal or unsupported transform: $transform');
|
||||
}
|
||||
final Iterable<Match> matches =_transformCommand.allMatches(transform).toList().reversed;
|
||||
final Iterable<Match> matches = _transformCommand.allMatches(transform).toList().reversed;
|
||||
Matrix3 result = Matrix3.identity();
|
||||
for (final Match m in matches) {
|
||||
final String command = m.group(1)!;
|
||||
@@ -539,19 +537,19 @@ int parsePixels(String pixels) {
|
||||
if (!_pixelsExp.hasMatch(pixels)) {
|
||||
throw ArgumentError(
|
||||
"illegal pixels expression: '$pixels'"
|
||||
' (the tool currently only support pixel units).');
|
||||
' (the tool currently only support pixel units).',
|
||||
);
|
||||
}
|
||||
return int.parse(_pixelsExp.firstMatch(pixels)!.group(1)!);
|
||||
}
|
||||
|
||||
String _extractAttr(XmlElement element, String name) {
|
||||
try {
|
||||
return element.attributes.singleWhere((XmlAttribute x) => x.name.local == name)
|
||||
.value;
|
||||
return element.attributes.singleWhere((XmlAttribute x) => x.name.local == name).value;
|
||||
} catch (e) {
|
||||
throw ArgumentError(
|
||||
"Can't find a single '$name' attributes in ${element.name}, "
|
||||
'attributes were: ${element.attributes}'
|
||||
"Can't find a single '$name' attributes in ${element.name}, "
|
||||
'attributes were: ${element.attributes}',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -562,9 +560,10 @@ bool _hasAttr(XmlElement element, String name) {
|
||||
|
||||
XmlElement _extractSvgElement(XmlDocument document) {
|
||||
return document.children.singleWhere(
|
||||
(XmlNode node) => node.nodeType == XmlNodeType.ELEMENT &&
|
||||
_asElement(node).name.local == 'svg'
|
||||
) as XmlElement;
|
||||
(XmlNode node) =>
|
||||
node.nodeType == XmlNodeType.ELEMENT && _asElement(node).name.local == 'svg',
|
||||
)
|
||||
as XmlElement;
|
||||
}
|
||||
|
||||
XmlElement _asElement(XmlNode node) => node as XmlElement;
|
||||
|
||||
@@ -11,29 +11,24 @@ import 'package:path/path.dart' as path;
|
||||
import 'package:vitool/vitool.dart';
|
||||
|
||||
void main() {
|
||||
|
||||
test('parsePixels', () {
|
||||
expect(parsePixels('23px'), 23);
|
||||
expect(parsePixels('9px'), 9);
|
||||
expect(() { parsePixels('9pt'); }, throwsArgumentError);
|
||||
expect(() {
|
||||
parsePixels('9pt');
|
||||
}, throwsArgumentError);
|
||||
});
|
||||
|
||||
test('parsePoints', () {
|
||||
expect(parsePoints('1.0, 2.0'),
|
||||
const <Point<double>>[Point<double>(1.0, 2.0)],
|
||||
);
|
||||
expect(parsePoints('12.0, 34.0 5.0, 6.6'),
|
||||
const <Point<double>>[
|
||||
Point<double>(12.0, 34.0),
|
||||
Point<double>(5.0, 6.6),
|
||||
],
|
||||
);
|
||||
expect(parsePoints('12.0 34.0 5.0 6.6'),
|
||||
const <Point<double>>[
|
||||
Point<double>(12.0, 34.0),
|
||||
Point<double>(5.0, 6.6),
|
||||
],
|
||||
);
|
||||
expect(parsePoints('1.0, 2.0'), const <Point<double>>[Point<double>(1.0, 2.0)]);
|
||||
expect(parsePoints('12.0, 34.0 5.0, 6.6'), const <Point<double>>[
|
||||
Point<double>(12.0, 34.0),
|
||||
Point<double>(5.0, 6.6),
|
||||
]);
|
||||
expect(parsePoints('12.0 34.0 5.0 6.6'), const <Point<double>>[
|
||||
Point<double>(12.0, 34.0),
|
||||
Point<double>(5.0, 6.6),
|
||||
]);
|
||||
});
|
||||
|
||||
group('parseSvg', () {
|
||||
@@ -43,21 +38,20 @@ void main() {
|
||||
});
|
||||
|
||||
test('illegal SVGs', () {
|
||||
expect(
|
||||
() { interpretSvg(testAsset('illegal_svg_multiple_roots.svg')); },
|
||||
throwsA(anything),
|
||||
);
|
||||
expect(() {
|
||||
interpretSvg(testAsset('illegal_svg_multiple_roots.svg'));
|
||||
}, throwsA(anything));
|
||||
});
|
||||
|
||||
test('SVG size', () {
|
||||
expect(
|
||||
interpretSvg(testAsset('empty_svg_1_48x48.svg')).size,
|
||||
const Point<double>(48.0, 48.0),
|
||||
interpretSvg(testAsset('empty_svg_1_48x48.svg')).size,
|
||||
const Point<double>(48.0, 48.0),
|
||||
);
|
||||
|
||||
expect(
|
||||
interpretSvg(testAsset('empty_svg_2_100x50.svg')).size,
|
||||
const Point<double>(100.0, 50.0),
|
||||
interpretSvg(testAsset('empty_svg_2_100x50.svg')).size,
|
||||
const Point<double>(100.0, 50.0),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -79,13 +73,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('SVG illegal path', () {
|
||||
expect(
|
||||
() { interpretSvg(testAsset('illegal_path.svg')); },
|
||||
throwsA(anything),
|
||||
);
|
||||
expect(() {
|
||||
interpretSvg(testAsset('illegal_path.svg'));
|
||||
}, throwsA(anything));
|
||||
});
|
||||
|
||||
|
||||
test('SVG group', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bars_group.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
@@ -122,8 +114,7 @@ void main() {
|
||||
test('SVG group scale', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_scale.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
SvgPath(
|
||||
'path_1', <SvgPathCommand>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 9.5)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(24.0, 9.5)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(24.0, 14.5)]),
|
||||
@@ -137,40 +128,34 @@ void main() {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_rotate_scale.svg'));
|
||||
expect(frameData.paths, const <PathMatcher>[
|
||||
PathMatcher(
|
||||
SvgPath(
|
||||
'path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 0.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 48.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(19.0, 48.0)]),
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(19.0, 0.0)]),
|
||||
SvgPathCommand('Z', <Point<double>>[]),
|
||||
]),
|
||||
margin: precisionErrorTolerance,
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 0.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(29.0, 48.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(19.0, 48.0)]),
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(19.0, 0.0)]),
|
||||
SvgPathCommand('Z', <Point<double>>[]),
|
||||
]),
|
||||
margin: precisionErrorTolerance,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('SVG illegal transform', () {
|
||||
expect(
|
||||
() { interpretSvg(testAsset('illegal_transform.svg')); },
|
||||
throwsA(anything),
|
||||
);
|
||||
expect(() {
|
||||
interpretSvg(testAsset('illegal_transform.svg'));
|
||||
}, throwsA(anything));
|
||||
});
|
||||
|
||||
test('SVG group opacity', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_opacity.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]),
|
||||
SvgPathCommand('Z', <Point<double>>[]),
|
||||
],
|
||||
opacity: 0.5,
|
||||
),
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(0.0, 29.0)]),
|
||||
SvgPathCommand('Z', <Point<double>>[]),
|
||||
], opacity: 0.5),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -178,8 +163,7 @@ void main() {
|
||||
// This asset uses the relative 'l' command instead of 'L'.
|
||||
final FrameData frameData = interpretSvg(testAsset('horizontal_bar_relative.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
SvgPath(
|
||||
'path_1', <SvgPathCommand>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 19.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 19.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(48.0, 29.0)]),
|
||||
@@ -193,8 +177,7 @@ void main() {
|
||||
// This asset uses the relative 'l' command instead of 'L'.
|
||||
final FrameData frameData = interpretSvg(testAsset('close_path_in_middle.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
SvgPath(
|
||||
'path_1', <SvgPathCommand>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(50.0, 50.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(60.0, 50.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(60.0, 60.0)]),
|
||||
@@ -210,115 +193,96 @@ void main() {
|
||||
group('create PathAnimation', () {
|
||||
test('single path', () {
|
||||
const List<FrameData> frameData = <FrameData>[
|
||||
FrameData(
|
||||
Point<double>(10.0, 10.0),
|
||||
<SvgPath>[
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(10.0, 10.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
SvgPathCommand('L', <Point<double>>[Point<double>(10.0, 10.0)]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
expect(PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
)),
|
||||
expect(
|
||||
PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple paths', () {
|
||||
const List<FrameData> frameData = <FrameData>[
|
||||
FrameData(
|
||||
Point<double>(10.0, 10.0),
|
||||
<SvgPath>[
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
],
|
||||
),
|
||||
SvgPath(
|
||||
'path_2',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
]),
|
||||
SvgPath('path_2', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
expect(PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
)),
|
||||
expect(
|
||||
PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(PathAnimation.fromFrameData(frameData, 1),
|
||||
const PathAnimationMatcher(PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(5.0, 6.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
)),
|
||||
expect(
|
||||
PathAnimation.fromFrameData(frameData, 1),
|
||||
const PathAnimationMatcher(
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(5.0, 6.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple frames', () {
|
||||
const List<FrameData> frameData = <FrameData>[
|
||||
FrameData(
|
||||
Point<double>(10.0, 10.0),
|
||||
<SvgPath>[
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
],
|
||||
opacity: 0.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
FrameData(
|
||||
Point<double>(10.0, 10.0),
|
||||
<SvgPath>[
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(10.0, 10.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
], opacity: 0.5),
|
||||
]),
|
||||
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(10.0, 10.0)]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
expect(PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(0.0, 0.0),
|
||||
Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
)),
|
||||
expect(
|
||||
PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -326,45 +290,42 @@ void main() {
|
||||
group('create Animation', () {
|
||||
test('multiple paths', () {
|
||||
const List<FrameData> frameData = <FrameData>[
|
||||
FrameData(
|
||||
Point<double>(10.0, 10.0),
|
||||
<SvgPath>[
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
],
|
||||
),
|
||||
SvgPath(
|
||||
'path_1',
|
||||
<SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
FrameData(Point<double>(10.0, 10.0), <SvgPath>[
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(0.0, 0.0)]),
|
||||
]),
|
||||
SvgPath('path_1', <SvgPathCommand>[
|
||||
SvgPathCommand('M', <Point<double>>[Point<double>(5.0, 6.0)]),
|
||||
]),
|
||||
]),
|
||||
];
|
||||
final Animation animation = Animation.fromFrameData(frameData);
|
||||
expect(animation.paths[0],
|
||||
const PathAnimationMatcher(PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
)),
|
||||
expect(
|
||||
animation.paths[0],
|
||||
const PathAnimationMatcher(
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(animation.paths[1],
|
||||
const PathAnimationMatcher(PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(5.0, 6.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
)),
|
||||
expect(
|
||||
animation.paths[1],
|
||||
const PathAnimationMatcher(
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(5.0, 6.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[1.0],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(animation.size, const Point<double>(10.0, 10.0));
|
||||
@@ -373,230 +334,184 @@ void main() {
|
||||
|
||||
group('toDart', () {
|
||||
test('_PathMoveTo', () {
|
||||
const PathCommandAnimation command = PathCommandAnimation(
|
||||
'M',
|
||||
<List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(1.0, 2.0),
|
||||
Point<double>(3.0, 4.0),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(1.0, 2.0),\n'
|
||||
' const Offset(3.0, 4.0),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
const PathCommandAnimation command = PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(1.0, 2.0), Point<double>(3.0, 4.0)],
|
||||
]);
|
||||
|
||||
expect(
|
||||
command.toDart(),
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(1.0, 2.0),\n'
|
||||
' const Offset(3.0, 4.0),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathLineTo', () {
|
||||
const PathCommandAnimation command = PathCommandAnimation(
|
||||
'L',
|
||||
<List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(1.0, 2.0),
|
||||
Point<double>(3.0, 4.0),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(1.0, 2.0),\n'
|
||||
' const Offset(3.0, 4.0),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
const PathCommandAnimation command = PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(1.0, 2.0), Point<double>(3.0, 4.0)],
|
||||
]);
|
||||
|
||||
expect(
|
||||
command.toDart(),
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(1.0, 2.0),\n'
|
||||
' const Offset(3.0, 4.0),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathCubicTo', () {
|
||||
const PathCommandAnimation command = PathCommandAnimation(
|
||||
'C',
|
||||
<List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(16.0, 24.0),
|
||||
Point<double>(16.0, 10.0),
|
||||
],
|
||||
<Point<double>>[
|
||||
Point<double>(16.0, 25.0),
|
||||
Point<double>(16.0, 11.0),
|
||||
],
|
||||
<Point<double>>[
|
||||
Point<double>(40.0, 40.0),
|
||||
Point<double>(40.0, 40.0),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathCubicTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(16.0, 24.0),\n'
|
||||
' const Offset(16.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(16.0, 25.0),\n'
|
||||
' const Offset(16.0, 11.0),\n'
|
||||
' ],\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(40.0, 40.0),\n'
|
||||
' const Offset(40.0, 40.0),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
const PathCommandAnimation command = PathCommandAnimation('C', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(16.0, 24.0), Point<double>(16.0, 10.0)],
|
||||
<Point<double>>[Point<double>(16.0, 25.0), Point<double>(16.0, 11.0)],
|
||||
<Point<double>>[Point<double>(40.0, 40.0), Point<double>(40.0, 40.0)],
|
||||
]);
|
||||
|
||||
expect(
|
||||
command.toDart(),
|
||||
' const _PathCubicTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(16.0, 24.0),\n'
|
||||
' const Offset(16.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(16.0, 25.0),\n'
|
||||
' const Offset(16.0, 11.0),\n'
|
||||
' ],\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(40.0, 40.0),\n'
|
||||
' const Offset(40.0, 40.0),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathClose', () {
|
||||
const PathCommandAnimation command = PathCommandAnimation(
|
||||
'Z',
|
||||
<List<Point<double>>>[],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathClose(\n'
|
||||
' ),\n',
|
||||
const PathCommandAnimation command = PathCommandAnimation('Z', <List<Point<double>>>[]);
|
||||
|
||||
expect(
|
||||
command.toDart(),
|
||||
' const _PathClose(\n'
|
||||
' ),\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('Unsupported path command', () {
|
||||
const PathCommandAnimation command = PathCommandAnimation(
|
||||
'h',
|
||||
<List<Point<double>>>[],
|
||||
);
|
||||
const PathCommandAnimation command = PathCommandAnimation('h', <List<Point<double>>>[]);
|
||||
|
||||
expect(
|
||||
() { command.toDart(); },
|
||||
throwsA(anything),
|
||||
);
|
||||
expect(() {
|
||||
command.toDart();
|
||||
}, throwsA(anything));
|
||||
});
|
||||
|
||||
test('_PathFrames', () {
|
||||
const PathAnimation pathAnimation = PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(0.0, 0.0),
|
||||
Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(48.0, 10.0),
|
||||
Point<double>(0.0, 0.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(48.0, 10.0), Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
);
|
||||
|
||||
expect(pathAnimation.toDart(),
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(48.0, 10.0),\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
expect(
|
||||
pathAnimation.toDart(),
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(48.0, 10.0),\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('Animation', () {
|
||||
const Animation animation = Animation(
|
||||
Point<double>(48.0, 48.0),
|
||||
<PathAnimation>[
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(0.0, 0.0),
|
||||
Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(48.0, 10.0),
|
||||
Point<double>(0.0, 0.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
),
|
||||
const Animation animation = Animation(Point<double>(48.0, 48.0), <PathAnimation>[
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
PathCommandAnimation('L', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(48.0, 10.0), Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
),
|
||||
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[
|
||||
Point<double>(0.0, 0.0),
|
||||
Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
),
|
||||
]);
|
||||
PathAnimation(
|
||||
<PathCommandAnimation>[
|
||||
PathCommandAnimation('M', <List<Point<double>>>[
|
||||
<Point<double>>[Point<double>(0.0, 0.0), Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
],
|
||||
opacities: <double>[0.5, 1.0],
|
||||
),
|
||||
]);
|
||||
|
||||
expect(animation.toDart('_AnimatedIconData', r'_$data1'),
|
||||
'const _AnimatedIconData _\$data1 = const _AnimatedIconData(\n'
|
||||
' const Size(48.0, 48.0),\n'
|
||||
' const <_PathFrames>[\n'
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(48.0, 10.0),\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
');',
|
||||
expect(
|
||||
animation.toDart('_AnimatedIconData', r'_$data1'),
|
||||
'const _AnimatedIconData _\$data1 = const _AnimatedIconData(\n'
|
||||
' const Size(48.0, 48.0),\n'
|
||||
' const <_PathFrames>[\n'
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(48.0, 10.0),\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
');',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -700,8 +615,8 @@ class PathAnimationMatcher extends Matcher {
|
||||
for (int i = 0; i < other.points.length; i += 1) {
|
||||
if (!const ListEquality<Point<double>>().equals(other.points[i], expected.points[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user