Skip to content

Commit

Permalink
[SuperTextField] Fix text capitalization configuration (Resolves #1617)…
Browse files Browse the repository at this point in the history
… (#1619)
  • Loading branch information
angelosilvestre authored and matthew-carroll committed Nov 28, 2023
1 parent e6cfe5b commit 5a12893
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:collection/collection.dart';
import 'package:flutter/services.dart';

extension TextInputConfigurationEquivalency on TextInputConfiguration {
/// Whether this [TextInputConfiguration] is equivalent to [other].
///
/// Two [TextInputConfiguration]s are considered to be equal
/// if all properties are equal.
bool isEquivalentTo(TextInputConfiguration other) {
return inputType == other.inputType &&
readOnly == other.readOnly &&
obscureText == other.obscureText &&
autocorrect == other.autocorrect &&
autofillConfiguration.isEquivalentTo(other.autofillConfiguration) &&
smartDashesType == other.smartDashesType &&
smartQuotesType == other.smartQuotesType &&
enableSuggestions == other.enableSuggestions &&
enableInteractiveSelection == other.enableInteractiveSelection &&
actionLabel == other.actionLabel &&
inputAction == other.inputAction &&
textCapitalization == other.textCapitalization &&
keyboardAppearance == other.keyboardAppearance &&
enableIMEPersonalizedLearning == other.enableIMEPersonalizedLearning &&
enableDeltaModel == other.enableDeltaModel &&
const DeepCollectionEquality().equals(allowedMimeTypes, other.allowedMimeTypes);
}
}

extension AutofillConfigurationEquivalency on AutofillConfiguration {
/// Whether this [AutofillConfiguration] is equivalent to [other].
///
/// Two [AutofillConfiguration]s are considered to be equal
/// if all properties are equal.
///
/// The [currentEditingValue] isn't considered in the comparison.
/// Otherwise, whenever the user changes the text or selection
/// would result in two configurations being unequal.
bool isEquivalentTo(AutofillConfiguration other) {
return enabled == other.enabled &&
uniqueIdentifier == other.uniqueIdentifier &&
const DeepCollectionEquality().equals(autofillHints, other.autofillHints) &&
hintText == other.hintText;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
import 'package:super_editor/src/infrastructure/flutter/text_input_configuration.dart';
import 'package:super_editor/src/infrastructure/focus.dart';
import 'package:super_editor/src/infrastructure/ime_input_owner.dart';
import 'package:super_editor/src/infrastructure/platforms/android/toolbar.dart';
Expand Down Expand Up @@ -223,13 +224,15 @@ class SuperAndroidTextFieldState extends State<SuperAndroidTextField>

if (widget.imeConfiguration != oldWidget.imeConfiguration &&
widget.imeConfiguration != null &&
(oldWidget.imeConfiguration == null || !widget.imeConfiguration!.isEquivalentTo(oldWidget.imeConfiguration!)) &&
_textEditingController.isAttachedToIme) {
_textEditingController.updateTextInputConfiguration(
textInputAction: widget.imeConfiguration!.inputAction,
textInputType: widget.imeConfiguration!.inputType,
autocorrect: widget.imeConfiguration!.autocorrect,
enableSuggestions: widget.imeConfiguration!.enableSuggestions,
keyboardAppearance: widget.imeConfiguration!.keyboardAppearance,
textCapitalization: widget.imeConfiguration!.textCapitalization,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:super_editor/src/core/document_layout.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
import 'package:super_editor/src/infrastructure/flutter/text_input_configuration.dart';
import 'package:super_editor/src/infrastructure/focus.dart';
import 'package:super_editor/src/infrastructure/ime_input_owner.dart';
import 'package:super_editor/src/infrastructure/keyboard.dart';
Expand Down Expand Up @@ -1134,13 +1135,15 @@ class _SuperTextFieldImeInteractorState extends State<SuperTextFieldImeInteracto

if (widget.imeConfiguration != oldWidget.imeConfiguration &&
widget.imeConfiguration != null &&
(oldWidget.imeConfiguration == null || !widget.imeConfiguration!.isEquivalentTo(oldWidget.imeConfiguration!)) &&
_textController.isAttachedToIme) {
_textController.updateTextInputConfiguration(
textInputAction: widget.imeConfiguration!.inputAction,
textInputType: widget.imeConfiguration!.inputType,
autocorrect: widget.imeConfiguration!.autocorrect,
enableSuggestions: widget.imeConfiguration!.enableSuggestions,
keyboardAppearance: widget.imeConfiguration!.keyboardAppearance,
textCapitalization: widget.imeConfiguration!.textCapitalization,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class ImeAttributedTextEditingController extends AttributedTextEditingController
TextInputAction textInputAction = TextInputAction.done,
TextInputType textInputType = TextInputType.text,
Brightness keyboardAppearance = Brightness.light,
TextCapitalization textCapitalization = TextCapitalization.none,
}) {
// Change the keyboard appearance even if we are detached from the IME.
// In the next time we attach to the IME, the keyboard appearance is used.
Expand All @@ -180,6 +181,7 @@ class ImeAttributedTextEditingController extends AttributedTextEditingController
inputAction: textInputAction,
inputType: textInputType,
keyboardAppearance: keyboardAppearance,
textCapitalization: textCapitalization,
);
final inputConnection = _inputConnectionFactory?.call(this, imeConfig) ?? TextInput.attach(this, imeConfig);
inputConnection.show();
Expand Down
3 changes: 3 additions & 0 deletions super_editor/lib/src/super_textfield/ios/ios_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:follow_the_leader/follow_the_leader.dart';
import 'package:super_editor/src/infrastructure/_logging.dart';
import 'package:super_editor/src/infrastructure/attributed_text_styles.dart';
import 'package:super_editor/src/infrastructure/flutter/flutter_scheduler.dart';
import 'package:super_editor/src/infrastructure/flutter/text_input_configuration.dart';
import 'package:super_editor/src/infrastructure/focus.dart';
import 'package:super_editor/src/infrastructure/ime_input_owner.dart';
import 'package:super_editor/src/infrastructure/platforms/ios/toolbar.dart';
Expand Down Expand Up @@ -257,13 +258,15 @@ class SuperIOSTextFieldState extends State<SuperIOSTextField>

if (widget.imeConfiguration != oldWidget.imeConfiguration &&
widget.imeConfiguration != null &&
(oldWidget.imeConfiguration == null || !widget.imeConfiguration!.isEquivalentTo(oldWidget.imeConfiguration!)) &&
_textEditingController.isAttachedToIme) {
_textEditingController.updateTextInputConfiguration(
textInputAction: widget.imeConfiguration!.inputAction,
textInputType: widget.imeConfiguration!.inputType,
autocorrect: widget.imeConfiguration!.autocorrect,
enableSuggestions: widget.imeConfiguration!.enableSuggestions,
keyboardAppearance: widget.imeConfiguration!.keyboardAppearance,
textCapitalization: widget.imeConfiguration!.textCapitalization,
);
}

Expand Down
152 changes: 151 additions & 1 deletion super_editor/test/super_textfield/super_textfield_ime_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test_robots/flutter_test_robots.dart';
import 'package:flutter_test_runners/flutter_test_runners.dart';
import 'package:super_editor/src/infrastructure/platforms/mac/mac_ime.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_editor/super_editor_test.dart';

Expand Down Expand Up @@ -529,6 +528,157 @@ void main() {
});
});

testWidgetsOnAllPlatforms('updates IME configuration when it changes', (tester) async {
final brightnessNotifier = ValueNotifier(Brightness.dark);

// Pump a SuperTextField with an IME configuration with values
// that differ from the defaults.
await tester.pumpWidget(
_buildScaffold(
child: ValueListenableBuilder(
valueListenable: brightnessNotifier,
builder: (context, brightness, child) {
return SuperTextField(
inputSource: TextInputSource.ime,
imeConfiguration: TextInputConfiguration(
enableSuggestions: false,
autocorrect: false,
inputAction: TextInputAction.search,
keyboardAppearance: brightness,
inputType: TextInputType.number,
enableDeltaModel: false,
textCapitalization: TextCapitalization.characters,
),
);
},
),
),
);

// Holds the IME configuration values passed to the platform.
String? inputAction;
String? inputType;
bool? autocorrect;
bool? enableSuggestions;
String? keyboardAppearance;
bool? enableDeltaModel;
String? textCapitalization;

// Intercept the setClient message sent to the platform to check the configuration.
tester
.interceptChannel(SystemChannels.textInput.name) //
.interceptMethod(
'TextInput.setClient',
(methodCall) {
final params = methodCall.arguments[1] as Map;
inputAction = params['inputAction'];
autocorrect = params['autocorrect'];
enableSuggestions = params['enableSuggestions'];
keyboardAppearance = params['keyboardAppearance'];
enableDeltaModel = params['enableDeltaModel'];
textCapitalization = params['textCapitalization'];

final inputTypeConfig = params['inputType'] as Map;
inputType = inputTypeConfig['name'];

return null;
},
);

// Tap to focus the text field and attach to the IME.
await tester.placeCaretInSuperTextField(0);

// Ensure we use the values from the configuration.
expect(inputAction, 'TextInputAction.search');
expect(inputType, 'TextInputType.number');
expect(autocorrect, false);
expect(enableSuggestions, false);
expect(enableDeltaModel, true);
expect(textCapitalization, 'TextCapitalization.characters');
expect(keyboardAppearance, 'Brightness.dark');

// Change the brightness to rebuild the widget
// and re-attach to the IME.
brightnessNotifier.value = Brightness.light;
await tester.pump();

// Ensure we use the values from the configuration,
// updating only the keyboard appearance.
expect(inputAction, 'TextInputAction.search');
expect(inputType, 'TextInputType.number');
expect(autocorrect, false);
expect(enableSuggestions, false);
expect(enableDeltaModel, true);
expect(textCapitalization, 'TextCapitalization.characters');
expect(keyboardAppearance, 'Brightness.light');
});

testWidgetsOnAllPlatforms('doesn\'t re-attach to IME if the configuration doesn\'t change', (tester) async {
// Keeps track of how many times TextInput.setClient was called.
int imeConnectionCount = 0;

// Explicitly avoid using const to ensure that we have two
// TextInputConfiguration instances with the same values.
//
// ignore: prefer_const_constructors
final configuration1 = TextInputConfiguration(
enableSuggestions: false,
autocorrect: false,
inputAction: TextInputAction.search,
keyboardAppearance: Brightness.dark,
inputType: TextInputType.number,
enableDeltaModel: false,
);
// ignore: prefer_const_constructors
final configuration2 = TextInputConfiguration(
enableSuggestions: false,
autocorrect: false,
inputAction: TextInputAction.search,
keyboardAppearance: Brightness.dark,
inputType: TextInputType.number,
enableDeltaModel: false,
);

final inputConfigurationNotifier = ValueNotifier(configuration1);

// Pump a SuperTextField with an IME configuration with values
// that differ from the defaults.
await tester.pumpWidget(
_buildScaffold(
child: ValueListenableBuilder(
valueListenable: inputConfigurationNotifier,
builder: (context, inputConfiguration, child) {
return SuperTextField(
inputSource: TextInputSource.ime,
imeConfiguration: inputConfiguration,
);
},
),
),
);

// Intercept the setClient message sent to the platform.
tester
.interceptChannel(SystemChannels.textInput.name) //
.interceptMethod(
'TextInput.setClient',
(methodCall) {
imeConnectionCount += 1;
return null;
},
);

// Tap to focus the text field and attach to the IME.
await tester.placeCaretInSuperTextField(0);

// Change the configuration instance to trigger a rebuild.
inputConfigurationNotifier.value = configuration2;
await tester.pump();

// Ensure the connection was performed only once.
expect(imeConnectionCount, 1);
});

group('SuperTextField on some bad Android software keyboards', () {
testWidgetsOnAndroid('handles BACKSPACE key event instead of deletion for a collapsed selection (on Android)',
(tester) async {
Expand Down

0 comments on commit 5a12893

Please sign in to comment.