Unverified Commit 071635b4 authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[CP-stable] Remove `TextEditingController` private member access (#149042) (#149208)

Manual request (merge conflicts in the test file) for cherry-picking #149042 to stable

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

#148692 

### Changelog Description:
Explain this cherry pick in one line that is accessible to most Flutter developers. See [best practices](https://github.com/flutter/flutter/wiki/Hotfix-Documentation-Best-Practices) for examples

Fixes an `EditableText` crash that happens with a custom `TextEditingController` which only `implements` the `TextEditingController` interface.

### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)? Does it impact development (ex. flutter doctor crashes when Android Studio is installed), or the shipping production app (the app crashes on launch)

Production apps crash if the user gives an `EditableText` a custom `TextEditingControllor` class that `implements TextEditingControllor`.

### Workaround:
Is there a workaround for this issue?

The crash does not happen if the user `extends TextEditingControllor`, but it's not always desirable / feasible. The class could be already extending a different base class.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

Run the test in the PR, or follow the repro steps in https://github.com/flutter/flutter/issues/148692
parent 2ac7536e
......@@ -311,7 +311,7 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
/// If the new selection is outside the composing range, the composing range is
/// cleared.
set selection(TextSelection newSelection) {
if (!_isSelectionWithinTextBounds(newSelection)) {
if (text.length < newSelection.end || text.length < newSelection.start) {
throw FlutterError('invalid text selection: $newSelection');
}
final TextRange newComposing = _isSelectionWithinComposingRange(newSelection) ? value.composing : TextRange.empty;
......@@ -345,11 +345,6 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
value = value.copyWith(composing: TextRange.empty);
}
/// Check that the [selection] is inside of the bounds of [text].
bool _isSelectionWithinTextBounds(TextSelection selection) {
return selection.start <= text.length && selection.end <= text.length;
}
/// Check that the [selection] is inside of the composing range.
bool _isSelectionWithinComposingRange(TextSelection selection) {
return selection.start >= value.composing.start && selection.end <= value.composing.end;
......@@ -3931,7 +3926,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// We return early if the selection is not valid. This can happen when the
// text of [EditableText] is updated at the same time as the selection is
// changed by a gesture event.
if (!widget.controller._isSelectionWithinTextBounds(selection)) {
final String text = widget.controller.value.text;
if (text.length < selection.end || text.length < selection.start) {
return;
}
......
......@@ -17248,6 +17248,27 @@ void main() {
await tester.pumpAndSettle();
expect(scrollController.offset, 75.0);
});
testWidgets('Can implement TextEditingController', (WidgetTester tester) async {
await tester.pumpWidget(
MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
autofocus: true,
backgroundCursorColor: Colors.grey,
controller: _TextEditingControllerImpl(),
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
),
),
);
expect(tester.takeException(), isNull);
});
}
class UnsettableController extends TextEditingController {
......@@ -17610,6 +17631,36 @@ class _AccentColorTextEditingController extends TextEditingController {
}
}
class _TextEditingControllerImpl extends ChangeNotifier implements TextEditingController {
final TextEditingController _innerController = TextEditingController();
@override
void clear() => _innerController.clear();
@override
void clearComposing() => _innerController.clearComposing();
@override
TextSelection get selection => _innerController.selection;
@override
set selection(TextSelection newSelection) => _innerController.selection = newSelection;
@override
String get text => _innerController.text;
@override
set text(String newText) => _innerController.text = newText;
@override
TextSpan buildTextSpan({required BuildContext context, TextStyle? style, required bool withComposing}) {
return _innerController.buildTextSpan(context: context, style: style, withComposing: withComposing);
}
@override
TextEditingValue get value => _innerController.value;
@override
set value(TextEditingValue newValue) => _innerController.value = newValue;
}
class _TestScrollController extends ScrollController {
bool get attached => hasListeners;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment