Unverified Commit cf09d993 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Handle invalid selection in TextEditingActionTarget (#90826)

Prevents bugs related to invalid (-1,-1) selection in keyboard shortcuts
parent 2367a177
...@@ -322,7 +322,7 @@ abstract class TextEditingActionTarget { ...@@ -322,7 +322,7 @@ abstract class TextEditingActionTarget {
/// ///
/// If the selection is not collapsed, deletes the selection. /// If the selection is not collapsed, deletes the selection.
/// ///
/// If [readOnly] is true, does nothing. /// If [readOnly] is true or the selection is invalid, does nothing.
/// ///
/// {@template flutter.widgets.TextEditingActionTarget.cause} /// {@template flutter.widgets.TextEditingActionTarget.cause}
/// The given [SelectionChangedCause] indicates the cause of this change and /// The given [SelectionChangedCause] indicates the cause of this change and
...@@ -336,6 +336,9 @@ abstract class TextEditingActionTarget { ...@@ -336,6 +336,9 @@ abstract class TextEditingActionTarget {
if (readOnly) { if (readOnly) {
return; return;
} }
if (!textEditingValue.selection.isValid) {
return;
}
// `delete` does not depend on the text layout, and the boundary analysis is // `delete` does not depend on the text layout, and the boundary analysis is
// done using the `previousCharacter` method instead of ICU, we can keep // done using the `previousCharacter` method instead of ICU, we can keep
...@@ -356,7 +359,7 @@ abstract class TextEditingActionTarget { ...@@ -356,7 +359,7 @@ abstract class TextEditingActionTarget {
/// ///
/// If the selection is not collapsed, deletes the selection. /// If the selection is not collapsed, deletes the selection.
/// ///
/// If [readOnly] is true, does nothing. /// If [readOnly] is true or the selection is invalid, does nothing.
/// ///
/// If [obscureText] is true, it treats the whole text content as a single /// If [obscureText] is true, it treats the whole text content as a single
/// word. /// word.
...@@ -377,6 +380,9 @@ abstract class TextEditingActionTarget { ...@@ -377,6 +380,9 @@ abstract class TextEditingActionTarget {
if (readOnly) { if (readOnly) {
return; return;
} }
if (!textEditingValue.selection.isValid) {
return;
}
if (obscureText) { if (obscureText) {
// When the text is obscured, the whole thing is treated as one big line. // When the text is obscured, the whole thing is treated as one big line.
...@@ -400,7 +406,7 @@ abstract class TextEditingActionTarget { ...@@ -400,7 +406,7 @@ abstract class TextEditingActionTarget {
/// If [obscureText] is true, it treats the whole text content as /// If [obscureText] is true, it treats the whole text content as
/// a single word. /// a single word.
/// ///
/// If [readOnly] is true, does nothing. /// If [readOnly] is true or the selection is invalid, does nothing.
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
...@@ -411,6 +417,9 @@ abstract class TextEditingActionTarget { ...@@ -411,6 +417,9 @@ abstract class TextEditingActionTarget {
if (readOnly) { if (readOnly) {
return; return;
} }
if (!textEditingValue.selection.isValid) {
return;
}
// When there is a line break, line delete shouldn't do anything // When there is a line break, line delete shouldn't do anything
final String textBefore = textEditingValue.selection.textBefore(textEditingValue.text); final String textBefore = textEditingValue.selection.textBefore(textEditingValue.text);
...@@ -439,7 +448,7 @@ abstract class TextEditingActionTarget { ...@@ -439,7 +448,7 @@ abstract class TextEditingActionTarget {
/// ///
/// If the selection is not collapsed, deletes the selection. /// If the selection is not collapsed, deletes the selection.
/// ///
/// If [readOnly] is true, does nothing. /// If [readOnly] is true or the selection is invalid, does nothing.
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
...@@ -450,6 +459,9 @@ abstract class TextEditingActionTarget { ...@@ -450,6 +459,9 @@ abstract class TextEditingActionTarget {
if (readOnly) { if (readOnly) {
return; return;
} }
if (!textEditingValue.selection.isValid) {
return;
}
final String textAfter = textEditingValue.selection.textAfter(textEditingValue.text); final String textAfter = textEditingValue.selection.textAfter(textEditingValue.text);
final int characterBoundary = nextCharacter(0, textAfter); final int characterBoundary = nextCharacter(0, textAfter);
...@@ -462,7 +474,7 @@ abstract class TextEditingActionTarget { ...@@ -462,7 +474,7 @@ abstract class TextEditingActionTarget {
/// ///
/// If the selection is not collapsed, deletes the selection. /// If the selection is not collapsed, deletes the selection.
/// ///
/// If [readOnly] is true, does nothing. /// If [readOnly] is true or the selection is invalid, does nothing.
/// ///
/// If [obscureText] is true, it treats the whole text content as /// If [obscureText] is true, it treats the whole text content as
/// a single word. /// a single word.
...@@ -479,6 +491,9 @@ abstract class TextEditingActionTarget { ...@@ -479,6 +491,9 @@ abstract class TextEditingActionTarget {
if (readOnly) { if (readOnly) {
return; return;
} }
if (!textEditingValue.selection.isValid) {
return;
}
if (obscureText) { if (obscureText) {
// When the text is obscured, the whole thing is treated as one big word. // When the text is obscured, the whole thing is treated as one big word.
...@@ -498,7 +513,7 @@ abstract class TextEditingActionTarget { ...@@ -498,7 +513,7 @@ abstract class TextEditingActionTarget {
/// ///
/// If the selection is not collapsed, deletes the selection. /// If the selection is not collapsed, deletes the selection.
/// ///
/// If [readOnly] is true, does nothing. /// If [readOnly] is true or the selection is invalid, does nothing.
/// ///
/// If [obscureText] is true, it treats the whole text content as /// If [obscureText] is true, it treats the whole text content as
/// a single word. /// a single word.
...@@ -512,6 +527,9 @@ abstract class TextEditingActionTarget { ...@@ -512,6 +527,9 @@ abstract class TextEditingActionTarget {
if (readOnly) { if (readOnly) {
return; return;
} }
if (!textEditingValue.selection.isValid) {
return;
}
if (obscureText) { if (obscureText) {
// When the text is obscured, the whole thing is treated as one big line. // When the text is obscured, the whole thing is treated as one big line.
...@@ -539,10 +557,18 @@ abstract class TextEditingActionTarget { ...@@ -539,10 +557,18 @@ abstract class TextEditingActionTarget {
/// The given SelectionChangedCause indicates the cause of this change and /// The given SelectionChangedCause indicates the cause of this change and
/// will be passed to setSelection. /// will be passed to setSelection.
/// ///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// See also: /// See also:
/// * [deleteToStart] /// * [deleteToStart]
void deleteToEnd(SelectionChangedCause cause) { void deleteToEnd(SelectionChangedCause cause) {
assert(textEditingValue.selection.isCollapsed); assert(textEditingValue.selection.isCollapsed);
if (readOnly) {
return;
}
if (!textEditingValue.selection.isValid) {
return;
}
setTextEditingValue(_deleteTo(TextPosition(offset: textEditingValue.text.length)), cause); setTextEditingValue(_deleteTo(TextPosition(offset: textEditingValue.text.length)), cause);
} }
...@@ -552,10 +578,18 @@ abstract class TextEditingActionTarget { ...@@ -552,10 +578,18 @@ abstract class TextEditingActionTarget {
/// The given SelectionChangedCause indicates the cause of this change and /// The given SelectionChangedCause indicates the cause of this change and
/// will be passed to setSelection. /// will be passed to setSelection.
/// ///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// See also: /// See also:
/// * [deleteToEnd] /// * [deleteToEnd]
void deleteToStart(SelectionChangedCause cause) { void deleteToStart(SelectionChangedCause cause) {
assert(textEditingValue.selection.isCollapsed); assert(textEditingValue.selection.isCollapsed);
if (readOnly) {
return;
}
if (!textEditingValue.selection.isValid) {
return;
}
setTextEditingValue(_deleteTo(const TextPosition(offset: 0)), cause); setTextEditingValue(_deleteTo(const TextPosition(offset: 0)), cause);
} }
...@@ -569,12 +603,17 @@ abstract class TextEditingActionTarget { ...@@ -569,12 +603,17 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// to the end. /// to the end.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [expandSelectionToStart], which is same but in the opposite direction. /// * [expandSelectionToStart], which is same but in the opposite direction.
void expandSelectionToEnd(SelectionChangedCause cause) { void expandSelectionToEnd(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionToEnd(cause); return moveSelectionToEnd(cause);
} }
...@@ -595,6 +634,8 @@ abstract class TextEditingActionTarget { ...@@ -595,6 +634,8 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// to the start. /// to the start.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -602,6 +643,9 @@ abstract class TextEditingActionTarget { ...@@ -602,6 +643,9 @@ abstract class TextEditingActionTarget {
/// * [expandSelectionToEnd], which is the same but in the opposite /// * [expandSelectionToEnd], which is the same but in the opposite
/// direction. /// direction.
void expandSelectionToStart(SelectionChangedCause cause) { void expandSelectionToStart(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionToStart(cause); return moveSelectionToStart(cause);
} }
...@@ -623,6 +667,8 @@ abstract class TextEditingActionTarget { ...@@ -623,6 +667,8 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left by line. /// left by line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -630,6 +676,9 @@ abstract class TextEditingActionTarget { ...@@ -630,6 +676,9 @@ abstract class TextEditingActionTarget {
/// * [expandSelectionRightByLine], which is the same but in the opposite /// * [expandSelectionRightByLine], which is the same but in the opposite
/// direction. /// direction.
void expandSelectionLeftByLine(SelectionChangedCause cause) { void expandSelectionLeftByLine(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionLeftByLine(cause); return moveSelectionLeftByLine(cause);
} }
...@@ -666,6 +715,8 @@ abstract class TextEditingActionTarget { ...@@ -666,6 +715,8 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right by line. /// right by line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -673,6 +724,9 @@ abstract class TextEditingActionTarget { ...@@ -673,6 +724,9 @@ abstract class TextEditingActionTarget {
/// * [expandSelectionLeftByLine], which is the same but in the opposite /// * [expandSelectionLeftByLine], which is the same but in the opposite
/// direction. /// direction.
void expandSelectionRightByLine(SelectionChangedCause cause) { void expandSelectionRightByLine(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionRightByLine(cause); return moveSelectionRightByLine(cause);
} }
...@@ -707,12 +761,17 @@ abstract class TextEditingActionTarget { ...@@ -707,12 +761,17 @@ abstract class TextEditingActionTarget {
/// If selectionEnabled is false, keeps the selection collapsed and just /// If selectionEnabled is false, keeps the selection collapsed and just
/// moves it down. /// moves it down.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [extendSelectionUp], which is same but in the opposite direction. /// * [extendSelectionUp], which is same but in the opposite direction.
void extendSelectionDown(SelectionChangedCause cause) { void extendSelectionDown(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionDown(cause); return moveSelectionDown(cause);
} }
...@@ -747,12 +806,17 @@ abstract class TextEditingActionTarget { ...@@ -747,12 +806,17 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left. /// left.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [extendSelectionRight], which is same but in the opposite direction. /// * [extendSelectionRight], which is same but in the opposite direction.
void extendSelectionLeft(SelectionChangedCause cause) { void extendSelectionLeft(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionLeft(cause); return moveSelectionLeft(cause);
} }
...@@ -782,6 +846,8 @@ abstract class TextEditingActionTarget { ...@@ -782,6 +846,8 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left by line. /// left by line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -791,6 +857,9 @@ abstract class TextEditingActionTarget { ...@@ -791,6 +857,9 @@ abstract class TextEditingActionTarget {
/// * [expandSelectionRightByLine], which strictly grows the selection /// * [expandSelectionRightByLine], which strictly grows the selection
/// regardless of the order. /// regardless of the order.
void extendSelectionLeftByLine(SelectionChangedCause cause) { void extendSelectionLeftByLine(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionLeftByLine(cause); return moveSelectionLeftByLine(cause);
} }
...@@ -828,12 +897,17 @@ abstract class TextEditingActionTarget { ...@@ -828,12 +897,17 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right. /// right.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [extendSelectionLeft], which is same but in the opposite direction. /// * [extendSelectionLeft], which is same but in the opposite direction.
void extendSelectionRight(SelectionChangedCause cause) { void extendSelectionRight(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionRight(cause); return moveSelectionRight(cause);
} }
...@@ -860,6 +934,8 @@ abstract class TextEditingActionTarget { ...@@ -860,6 +934,8 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right by line. /// right by line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -869,6 +945,9 @@ abstract class TextEditingActionTarget { ...@@ -869,6 +945,9 @@ abstract class TextEditingActionTarget {
/// * [expandSelectionRightByLine], which strictly grows the selection /// * [expandSelectionRightByLine], which strictly grows the selection
/// regardless of the order. /// regardless of the order.
void extendSelectionRightByLine(SelectionChangedCause cause) { void extendSelectionRightByLine(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionRightByLine(cause); return moveSelectionRightByLine(cause);
} }
...@@ -898,6 +977,8 @@ abstract class TextEditingActionTarget { ...@@ -898,6 +977,8 @@ abstract class TextEditingActionTarget {
/// Extend the current selection to the previous start of a word. /// Extend the current selection to the previous start of a word.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace} /// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
...@@ -915,6 +996,9 @@ abstract class TextEditingActionTarget { ...@@ -915,6 +996,9 @@ abstract class TextEditingActionTarget {
/// direction. /// direction.
void extendSelectionLeftByWord(SelectionChangedCause cause, void extendSelectionLeftByWord(SelectionChangedCause cause,
[bool includeWhitespace = true, bool stopAtReversal = false]) { [bool includeWhitespace = true, bool stopAtReversal = false]) {
if (!textEditingValue.selection.isValid) {
return;
}
// When the text is obscured, the whole thing is treated as one big word. // When the text is obscured, the whole thing is treated as one big word.
if (obscureText) { if (obscureText) {
return _extendSelectionToStart(cause); return _extendSelectionToStart(cause);
...@@ -946,6 +1030,8 @@ abstract class TextEditingActionTarget { ...@@ -946,6 +1030,8 @@ abstract class TextEditingActionTarget {
/// Extend the current selection to the next end of a word. /// Extend the current selection to the next end of a word.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace} /// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
...@@ -958,6 +1044,9 @@ abstract class TextEditingActionTarget { ...@@ -958,6 +1044,9 @@ abstract class TextEditingActionTarget {
/// direction. /// direction.
void extendSelectionRightByWord(SelectionChangedCause cause, void extendSelectionRightByWord(SelectionChangedCause cause,
[bool includeWhitespace = true, bool stopAtReversal = false]) { [bool includeWhitespace = true, bool stopAtReversal = false]) {
if (!textEditingValue.selection.isValid) {
return;
}
debugAssertLayoutUpToDate(); debugAssertLayoutUpToDate();
// When the text is obscured, the whole thing is treated as one big word. // When the text is obscured, the whole thing is treated as one big word.
if (obscureText) { if (obscureText) {
...@@ -997,6 +1086,8 @@ abstract class TextEditingActionTarget { ...@@ -997,6 +1086,8 @@ abstract class TextEditingActionTarget {
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it /// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// up. /// up.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -1004,6 +1095,9 @@ abstract class TextEditingActionTarget { ...@@ -1004,6 +1095,9 @@ abstract class TextEditingActionTarget {
/// * [extendSelectionDown], which is the same but in the opposite /// * [extendSelectionDown], which is the same but in the opposite
/// direction. /// direction.
void extendSelectionUp(SelectionChangedCause cause) { void extendSelectionUp(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
if (!selectionEnabled) { if (!selectionEnabled) {
return moveSelectionUp(cause); return moveSelectionUp(cause);
} }
...@@ -1043,6 +1137,8 @@ abstract class TextEditingActionTarget { ...@@ -1043,6 +1137,8 @@ abstract class TextEditingActionTarget {
/// Move the current selection to the leftmost point of the current line. /// Move the current selection to the leftmost point of the current line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -1050,6 +1146,9 @@ abstract class TextEditingActionTarget { ...@@ -1050,6 +1146,9 @@ abstract class TextEditingActionTarget {
/// * [moveSelectionRightByLine], which is the same but in the opposite /// * [moveSelectionRightByLine], which is the same but in the opposite
/// direction. /// direction.
void moveSelectionLeftByLine(SelectionChangedCause cause) { void moveSelectionLeftByLine(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
// If already at the left edge of the line, do nothing. // If already at the left edge of the line, do nothing.
final TextSelection currentLine = textLayoutMetrics.getLineAtOffset( final TextSelection currentLine = textLayoutMetrics.getLineAtOffset(
textEditingValue.selection.extent, textEditingValue.selection.extent,
...@@ -1079,12 +1178,17 @@ abstract class TextEditingActionTarget { ...@@ -1079,12 +1178,17 @@ abstract class TextEditingActionTarget {
/// ///
/// Move the current selection to the next line. /// Move the current selection to the next line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [moveSelectionUp], which is the same but in the opposite direction. /// * [moveSelectionUp], which is the same but in the opposite direction.
void moveSelectionDown(SelectionChangedCause cause) { void moveSelectionDown(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
// If the selection is collapsed at the end of the field already, then // If the selection is collapsed at the end of the field already, then
// nothing happens. // nothing happens.
if (textEditingValue.selection.isCollapsed && if (textEditingValue.selection.isCollapsed &&
...@@ -1118,12 +1222,17 @@ abstract class TextEditingActionTarget { ...@@ -1118,12 +1222,17 @@ abstract class TextEditingActionTarget {
/// ///
/// If it can't be moved left, do nothing. /// If it can't be moved left, do nothing.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [moveSelectionRight], which is the same but in the opposite direction. /// * [moveSelectionRight], which is the same but in the opposite direction.
void moveSelectionLeft(SelectionChangedCause cause) { void moveSelectionLeft(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
// If the selection is already all the way left, there is nothing to do. // If the selection is already all the way left, there is nothing to do.
if (textEditingValue.selection.isCollapsed && textEditingValue.selection.extentOffset <= 0) { if (textEditingValue.selection.isCollapsed && textEditingValue.selection.extentOffset <= 0) {
return; return;
...@@ -1156,6 +1265,8 @@ abstract class TextEditingActionTarget { ...@@ -1156,6 +1265,8 @@ abstract class TextEditingActionTarget {
/// A TextSelection that isn't collapsed will be collapsed and moved from the /// A TextSelection that isn't collapsed will be collapsed and moved from the
/// extentOffset. /// extentOffset.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace} /// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
...@@ -1166,6 +1277,9 @@ abstract class TextEditingActionTarget { ...@@ -1166,6 +1277,9 @@ abstract class TextEditingActionTarget {
/// direction. /// direction.
void moveSelectionLeftByWord(SelectionChangedCause cause, void moveSelectionLeftByWord(SelectionChangedCause cause,
[bool includeWhitespace = true]) { [bool includeWhitespace = true]) {
if (!textEditingValue.selection.isValid) {
return;
}
// When the text is obscured, the whole thing is treated as one big word. // When the text is obscured, the whole thing is treated as one big word.
if (obscureText) { if (obscureText) {
return moveSelectionToStart(cause); return moveSelectionToStart(cause);
...@@ -1189,7 +1303,7 @@ abstract class TextEditingActionTarget { ...@@ -1189,7 +1303,7 @@ abstract class TextEditingActionTarget {
/// Move the current selection to the right by one character. /// Move the current selection to the right by one character.
/// ///
/// If it can't be moved right, do nothing. /// If the selection is invalid or it can't be moved right, do nothing.
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
...@@ -1197,6 +1311,9 @@ abstract class TextEditingActionTarget { ...@@ -1197,6 +1311,9 @@ abstract class TextEditingActionTarget {
/// ///
/// * [moveSelectionLeft], which is the same but in the opposite direction. /// * [moveSelectionLeft], which is the same but in the opposite direction.
void moveSelectionRight(SelectionChangedCause cause) { void moveSelectionRight(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
// If the selection is already all the way right, there is nothing to do. // If the selection is already all the way right, there is nothing to do.
if (textEditingValue.selection.isCollapsed && if (textEditingValue.selection.isCollapsed &&
textEditingValue.selection.extentOffset >= textEditingValue.text.length) { textEditingValue.selection.extentOffset >= textEditingValue.text.length) {
...@@ -1224,6 +1341,8 @@ abstract class TextEditingActionTarget { ...@@ -1224,6 +1341,8 @@ abstract class TextEditingActionTarget {
/// ///
/// Move the current selection to the rightmost point of the current line. /// Move the current selection to the rightmost point of the current line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
...@@ -1231,6 +1350,9 @@ abstract class TextEditingActionTarget { ...@@ -1231,6 +1350,9 @@ abstract class TextEditingActionTarget {
/// * [moveSelectionLeftByLine], which is the same but in the opposite /// * [moveSelectionLeftByLine], which is the same but in the opposite
/// direction. /// direction.
void moveSelectionRightByLine(SelectionChangedCause cause) { void moveSelectionRightByLine(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
// If already at the right edge of the line, do nothing. // If already at the right edge of the line, do nothing.
final TextSelection currentLine = textLayoutMetrics.getLineAtOffset( final TextSelection currentLine = textLayoutMetrics.getLineAtOffset(
textEditingValue.selection.extent, textEditingValue.selection.extent,
...@@ -1263,6 +1385,8 @@ abstract class TextEditingActionTarget { ...@@ -1263,6 +1385,8 @@ abstract class TextEditingActionTarget {
/// A TextSelection that isn't collapsed will be collapsed and moved from the /// A TextSelection that isn't collapsed will be collapsed and moved from the
/// extentOffset. /// extentOffset.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace} /// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
...@@ -1273,6 +1397,9 @@ abstract class TextEditingActionTarget { ...@@ -1273,6 +1397,9 @@ abstract class TextEditingActionTarget {
/// direction. /// direction.
void moveSelectionRightByWord(SelectionChangedCause cause, void moveSelectionRightByWord(SelectionChangedCause cause,
[bool includeWhitespace = true]) { [bool includeWhitespace = true]) {
if (!textEditingValue.selection.isValid) {
return;
}
// When the text is obscured, the whole thing is treated as one big word. // When the text is obscured, the whole thing is treated as one big word.
if (obscureText) { if (obscureText) {
return moveSelectionToEnd(cause); return moveSelectionToEnd(cause);
...@@ -1328,12 +1455,17 @@ abstract class TextEditingActionTarget { ...@@ -1328,12 +1455,17 @@ abstract class TextEditingActionTarget {
/// Move the current selection up by one line. /// Move the current selection up by one line.
/// ///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
/// ///
/// See also: /// See also:
/// ///
/// * [moveSelectionDown], which is the same but in the opposite direction. /// * [moveSelectionDown], which is the same but in the opposite direction.
void moveSelectionUp(SelectionChangedCause cause) { void moveSelectionUp(SelectionChangedCause cause) {
if (!textEditingValue.selection.isValid) {
return;
}
final int nextIndex = final int nextIndex =
textLayoutMetrics.getTextPositionAbove(textEditingValue.selection.extent).offset; textLayoutMetrics.getTextPositionAbove(textEditingValue.selection.extent).offset;
...@@ -1363,12 +1495,14 @@ abstract class TextEditingActionTarget { ...@@ -1363,12 +1495,14 @@ abstract class TextEditingActionTarget {
/// Copy current selection to [Clipboard]. /// Copy current selection to [Clipboard].
/// {@endtemplate} /// {@endtemplate}
/// ///
/// If the selection is collapsed or invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
void copySelection(SelectionChangedCause cause) { void copySelection(SelectionChangedCause cause) {
final TextSelection selection = textEditingValue.selection; final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text; final String text = textEditingValue.text;
assert(selection != null); assert(selection != null);
if (selection.isCollapsed) { if (selection.isCollapsed || !selection.isValid) {
return; return;
} }
Clipboard.setData(ClipboardData(text: selection.textInside(text))); Clipboard.setData(ClipboardData(text: selection.textInside(text)));
...@@ -1378,12 +1512,14 @@ abstract class TextEditingActionTarget { ...@@ -1378,12 +1512,14 @@ abstract class TextEditingActionTarget {
/// Cut current selection to Clipboard. /// Cut current selection to Clipboard.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
void cutSelection(SelectionChangedCause cause) { void cutSelection(SelectionChangedCause cause) {
if (readOnly) { final TextSelection selection = textEditingValue.selection;
if (readOnly || !selection.isValid) {
return; return;
} }
final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text; final String text = textEditingValue.text;
assert(selection != null); assert(selection != null);
if (selection.isCollapsed) { if (selection.isCollapsed) {
...@@ -1408,12 +1544,14 @@ abstract class TextEditingActionTarget { ...@@ -1408,12 +1544,14 @@ abstract class TextEditingActionTarget {
/// ///
/// If there is currently a selection, it will be replaced. /// If there is currently a selection, it will be replaced.
/// ///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause} /// {@macro flutter.widgets.TextEditingActionTarget.cause}
Future<void> pasteText(SelectionChangedCause cause) async { Future<void> pasteText(SelectionChangedCause cause) async {
if (readOnly) { final TextSelection selection = textEditingValue.selection;
if (readOnly || !selection.isValid) {
return; return;
} }
final TextSelection selection = textEditingValue.selection;
final String text = textEditingValue.text; final String text = textEditingValue.text;
assert(selection != null); assert(selection != null);
if (!selection.isValid) { if (!selection.isValid) {
......
...@@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; ...@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/rendering_tester.dart'; import '../rendering/rendering_tester.dart';
import 'clipboard_utils.dart';
class _FakeEditableTextState with TextSelectionDelegate, TextEditingActionTarget { class _FakeEditableTextState with TextSelectionDelegate, TextEditingActionTarget {
_FakeEditableTextState({ _FakeEditableTextState({
...@@ -93,9 +94,24 @@ class _FakeEditableTextState with TextSelectionDelegate, TextEditingActionTarget ...@@ -93,9 +94,24 @@ class _FakeEditableTextState with TextSelectionDelegate, TextEditingActionTarget
} }
void main() { void main() {
final MockClipboard mockClipboard = MockClipboard();
// Ensure that all TestRenderingFlutterBinding bindings are initialized. // Ensure that all TestRenderingFlutterBinding bindings are initialized.
renderer; renderer;
setUp(() async {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
mockClipboard.handleMethodCall,
);
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
null,
);
});
test('moveSelectionLeft/RightByLine stays on the current line', () async { test('moveSelectionLeft/RightByLine stays on the current line', () async {
const String text = 'one two three\n\nfour five six'; const String text = 'one two three\n\nfour five six';
final _FakeEditableTextState editableTextState = _FakeEditableTextState( final _FakeEditableTextState editableTextState = _FakeEditableTextState(
...@@ -1712,6 +1728,134 @@ void main() { ...@@ -1712,6 +1728,134 @@ void main() {
}); });
}); });
test("When a selection is needed but it's invalid, nothing is changed", () async {
const String text = 'one two three\n\nfour five six';
final _FakeEditableTextState editableTextState = _FakeEditableTextState(
textSpan: const TextSpan(
text: text,
style: TextStyle(
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
),
),
textEditingValue: const TextEditingValue(
text: text,
selection: TextSelection.collapsed(offset: -1),
),
);
final RenderEditable editable = editableTextState.renderEditable;
layout(editable);
editable.hasFocus = true;
editableTextState.delete(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteForward(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteForwardByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteForwardByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteToEnd(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.deleteToStart(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
editableTextState.expandSelectionToEnd(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.expandSelectionToStart(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.expandSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.expandSelectionRightByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionDown(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeft(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeft(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionRight(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionRightByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionLeftByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionRightByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.extendSelectionUp(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionLeftByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionDown(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionLeft(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionLeftByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionRight(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionRightByLine(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionRightByWord(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.moveSelectionUp(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
editableTextState.copySelection(SelectionChangedCause.keyboard);
ClipboardData? clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
expect(clipboardData?.text, null);
editableTextState.cutSelection(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
expect(clipboardData?.text, null);
editableTextState.pasteText(SelectionChangedCause.keyboard);
expect(editableTextState.textEditingValue.selection.isValid, false);
expect(editableTextState.textEditingValue.text, text);
});
group('nextCharacter', () { group('nextCharacter', () {
test('handles normal strings correctly', () { test('handles normal strings correctly', () {
expect(TextEditingActionTarget.nextCharacter(0, '01234567'), 1); expect(TextEditingActionTarget.nextCharacter(0, '01234567'), 1);
......
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