diff --git a/.agents/rules.md b/.agents/rules.md index 6ad16833..3c71b38d 100644 --- a/.agents/rules.md +++ b/.agents/rules.md @@ -75,6 +75,16 @@ clear of the button on either side. `test/lint/typography_test.dart` fails on `textTheme` anywhere in `lib/` but `typography.dart`, and on `fontSize:` or `fontWeight:` outside `lib/ui/`, except in the code editor, whose text follows the editor font-size setting. +### Text Field Suggestions + +On Android, Flutter maps `enableSuggestions: false` to the visible-password input type, not only to "no suggestions" +(`TextInputPlugin.inputTypeFromTextInputType` in the engine). Input methods then treat the field as a password: no +clipboard panel, no Chinese layouts, and on some systems a secure keyboard. With the app's clipboard permission denied, +that leaves no way to paste a link at all. Only the secret fields, `SecretTextFormField` and `FormRowSecretTextField`, +ask for a password keyboard; a link, key, rule or regex field that should not be corrected sets `autocorrect: false` +alone. `test/lint/text_field_suggestions_test.dart` fails on `enableSuggestions: false` or +`TextInputType.visiblePassword` anywhere else in `lib/`. + ### Length Units Flutter lengths are logical pixels, the unit Android calls dp. Comments, test names, docs and reports give them in dp, diff --git a/lib/features/form/batch_section.dart b/lib/features/form/batch_section.dart index f5cc8173..1ed6caf5 100644 --- a/lib/features/form/batch_section.dart +++ b/lib/features/form/batch_section.dart @@ -36,7 +36,6 @@ class BatchSection extends StatelessWidget { maxLines: null, keyboardType: TextInputType.multiline, autocorrect: false, - enableSuggestions: false, onChanged: onChanged, decoration: InputDecoration.collapsed( border: const NoInputBorder(), diff --git a/lib/features/form/map_edit_view.dart b/lib/features/form/map_edit_view.dart index fc88cfbc..8cfef744 100644 --- a/lib/features/form/map_edit_view.dart +++ b/lib/features/form/map_edit_view.dart @@ -288,7 +288,6 @@ class _MapEntryFormState extends State<_MapEntryForm> { hintText: appLocalizations.required, textInputAction: TextInputAction.next, autocorrect: false, - enableSuggestions: false, inputFormatters: keyMaxLength == null ? null : TextInputLimits.limit(keyMaxLength), @@ -305,7 +304,6 @@ class _MapEntryFormState extends State<_MapEntryForm> { hintText: appLocalizations.required, textInputAction: TextInputAction.done, autocorrect: false, - enableSuggestions: false, inputFormatters: valueMaxLength == null ? null : TextInputLimits.limit(valueMaxLength), diff --git a/lib/features/form/named_url_form.dart b/lib/features/form/named_url_form.dart index bb1e35ce..09821caf 100644 --- a/lib/features/form/named_url_form.dart +++ b/lib/features/form/named_url_form.dart @@ -296,7 +296,6 @@ class _NamedUrlFormState extends State { ? TextInputAction.next : TextInputAction.done, autocorrect: false, - enableSuggestions: false, inputFormatters: TextInputLimits.limit(TextInputLimits.url), onChanged: _handleChanged, onFieldSubmitted: (_) { diff --git a/lib/features/form/rule.dart b/lib/features/form/rule.dart index 7e2604ae..d7c09753 100644 --- a/lib/features/form/rule.dart +++ b/lib/features/form/rule.dart @@ -374,7 +374,6 @@ class _RuleFormState extends State { controller: _contentController, hintText: appLocalizations.inputRuleContent, autocorrect: false, - enableSuggestions: false, inputFormatters: TextInputLimits.limit(TextInputLimits.rule), onChanged: (_) { if (_attempted) { diff --git a/lib/views/backup_and_restore.dart b/lib/views/backup_and_restore.dart index da095c9e..1d05b38e 100644 --- a/lib/views/backup_and_restore.dart +++ b/lib/views/backup_and_restore.dart @@ -441,7 +441,6 @@ class _WebDAVFormState extends ConsumerState { keyboardType: TextInputType.url, textInputAction: TextInputAction.next, autocorrect: false, - enableSuggestions: false, inputFormatters: TextInputLimits.limit(TextInputLimits.uri), onChanged: _handleChanged, ), @@ -454,7 +453,6 @@ class _WebDAVFormState extends ConsumerState { hintText: appLocalizations.required, textInputAction: TextInputAction.next, autocorrect: false, - enableSuggestions: false, inputFormatters: TextInputLimits.limit( TextInputLimits.userName, ), diff --git a/lib/views/config/filters.dart b/lib/views/config/filters.dart index 0d5070d6..bce3819f 100644 --- a/lib/views/config/filters.dart +++ b/lib/views/config/filters.dart @@ -269,7 +269,6 @@ class _FilterFormState extends State<_FilterForm> { controller: _regexController, hintText: r'(?i)hk|hong\s*kong', autocorrect: false, - enableSuggestions: false, textInputAction: TextInputAction.done, inputFormatters: TextInputLimits.limit(TextInputLimits.filter), onChanged: (_) { diff --git a/lib/views/config/providers.dart b/lib/views/config/providers.dart index eee56265..c6836cdc 100644 --- a/lib/views/config/providers.dart +++ b/lib/views/config/providers.dart @@ -624,7 +624,6 @@ class _ProviderFormState extends State<_ProviderForm> { keyboardType: TextInputType.url, textInputAction: TextInputAction.done, autocorrect: false, - enableSuggestions: false, onChanged: _handleChanged, onFieldSubmitted: (_) => _handleSubmit(), ), diff --git a/lib/views/profiles/edit.dart b/lib/views/profiles/edit.dart index 08abf3c0..d0527a1b 100644 --- a/lib/views/profiles/edit.dart +++ b/lib/views/profiles/edit.dart @@ -454,7 +454,6 @@ class _ProfileUrlField extends StatelessWidget { keyboardType: TextInputType.url, textInputAction: TextInputAction.next, autocorrect: false, - enableSuggestions: false, inputFormatters: TextInputLimits.limit(TextInputLimits.url), onChanged: onChanged, ), diff --git a/lib/widgets/palette.dart b/lib/widgets/palette.dart index c34e797b..92133d9c 100644 --- a/lib/widgets/palette.dart +++ b/lib/widgets/palette.dart @@ -703,7 +703,6 @@ class _HexColorFieldState extends State { child: TextField( controller: _controller, autocorrect: false, - enableSuggestions: false, textCapitalization: TextCapitalization.characters, style: const TextStyle(fontFeatures: [FontFeature.tabularFigures()]), decoration: InputDecoration( diff --git a/test/lint/text_field_suggestions_test.dart b/test/lint/text_field_suggestions_test.dart new file mode 100644 index 00000000..d977c878 --- /dev/null +++ b/test/lint/text_field_suggestions_test.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:test/test.dart'; + +final _passwordInputType = RegExp( + r'\benableSuggestions:\s*false\b|\bTextInputType\.visiblePassword\b', +); + +const _secretFields = {'lib/ui/input.dart', 'lib/features/form/form_row.dart'}; + +void main() { + test('only secret fields ask Android for a password keyboard', () { + final offenders = []; + + for (final entity in Directory('lib').listSync(recursive: true)) { + if (entity is! File || + !entity.path.endsWith('.dart') || + entity.path.contains('/generated/') || + _secretFields.contains(entity.path)) { + continue; + } + final lines = entity.readAsLinesSync(); + for (var i = 0; i < lines.length; i++) { + final match = _passwordInputType.firstMatch(lines[i]); + if (match != null) { + offenders.add( + '${entity.path}:${i + 1} — ${match[0]} makes Android input ' + 'methods treat the field as a password, with no clipboard or ' + 'Chinese input; keep autocorrect: false alone.', + ); + } + } + } + + expect(offenders, isEmpty, reason: offenders.join('\n')); + }); +}