Unverified Commit 47db96af authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Mac word modifier TextEditingActions (#78573)

parent 2c2c8a75
......@@ -817,13 +817,24 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// Return the given TextSelection extended left to the beginning of the
// nearest word.
static TextSelection _extendGivenSelectionLeftByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true]) {
//
// See extendSelectionLeftByWord for a detailed explanation of the two
// optional parameters.
static TextSelection _extendGivenSelectionLeftByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true, bool stopAtReversal = false]) {
// If the selection is already all the way left, there is nothing to do.
if (selection.isCollapsed && selection.extentOffset <= 0) {
return selection;
}
final int leftOffset = _getLeftByWord(textPainter, selection.extentOffset, includeWhitespace);
if (stopAtReversal && selection.extentOffset > selection.baseOffset
&& leftOffset < selection.baseOffset) {
return selection.copyWith(
extentOffset: selection.baseOffset,
);
}
return selection.copyWith(
extentOffset: leftOffset,
);
......@@ -831,7 +842,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
// Return the given TextSelection extended right to the end of the nearest
// word.
static TextSelection _extendGivenSelectionRightByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true]) {
//
// See extendSelectionRightByWord for a detailed explanation of the two
// optional parameters.
static TextSelection _extendGivenSelectionRightByWord(TextPainter textPainter, TextSelection selection, [bool includeWhitespace = true, bool stopAtReversal = false]) {
// If the selection is already all the way right, there is nothing to do.
final String text = textPainter.text!.toPlainText();
if (selection.isCollapsed && selection.extentOffset == text.length) {
......@@ -839,6 +853,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
final int rightOffset = _getRightByWord(textPainter, selection.extentOffset, includeWhitespace);
if (stopAtReversal && selection.baseOffset > selection.extentOffset
&& rightOffset > selection.baseOffset) {
return selection.copyWith(
extentOffset: selection.baseOffset,
);
}
return selection.copyWith(
extentOffset: rightOffset,
);
......@@ -1340,15 +1362,23 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// extended past any whitespace and the first word following the whitespace.
/// By default, `includeWhitespace` is set to true, meaning that whitespace
/// can be considered a word in itself. If set to false, the selection will
/// be extended past any whitespace and the first word following the
/// whitespace.
///
/// {@template flutter.rendering.RenderEditable.stopAtReversal}
/// The `stopAtReversal` parameter is false by default, meaning that it's
/// ok for the base and extent to flip their order here. If set to true, then
/// the selection will collapse when it would otherwise reverse its order. A
/// selection that is already collapsed is not affected by this parameter.
/// {@endtemplate}
///
/// See also:
///
/// * [extendSelectionRightByWord], which is the same but in the opposite
/// direction.
void extendSelectionLeftByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
void extendSelectionLeftByWord(SelectionChangedCause cause, [bool includeWhitespace = true, bool stopAtReversal = false]) {
assert(selection != null);
// When the text is obscured, the whole thing is treated as one big word.
......@@ -1363,6 +1393,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_textPainter,
selection!,
includeWhitespace,
stopAtReversal,
);
if (nextSelection == selection) {
return;
......@@ -1374,15 +1405,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
///
/// {@macro flutter.rendering.RenderEditable.cause}
///
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// extended past any whitespace and the first word following the whitespace.
/// By default, `includeWhitespace` is set to true, meaning that whitespace
/// can be considered a word in itself. If set to false, the selection will
/// be extended past any whitespace and the first word following the
/// whitespace.
///
/// {@macro flutter.rendering.RenderEditable.stopAtReversal}
///
/// See also:
///
/// * [extendSelectionLeftByWord], which is the same but in the opposite
/// direction.
void extendSelectionRightByWord(SelectionChangedCause cause, [bool includeWhitespace = true]) {
void extendSelectionRightByWord(SelectionChangedCause cause, [bool includeWhitespace = true, bool stopAtReversal = false]) {
assert(selection != null);
// When the text is obscured, the whole thing is treated as one big word.
......@@ -1397,6 +1431,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_textPainter,
selection!,
includeWhitespace,
stopAtReversal,
);
if (nextSelection == selection) {
return;
......
......@@ -41,7 +41,9 @@ class DefaultTextEditingActions extends Actions{
ExtendSelectionDownTextIntent: _ExtendSelectionDownTextAction(),
ExtendSelectionLeftByLineTextIntent: _ExtendSelectionLeftByLineTextAction(),
ExtendSelectionLeftByWordTextIntent: _ExtendSelectionLeftByWordTextAction(),
ExtendSelectionLeftByWordAndStopAtReversalTextIntent: _ExtendSelectionLeftByWordAndStopAtReversalTextAction(),
ExtendSelectionLeftTextIntent: _ExtendSelectionLeftTextAction(),
ExtendSelectionRightByWordAndStopAtReversalTextIntent: _ExtendSelectionRightByWordAndStopAtReversalTextAction(),
ExtendSelectionRightByWordTextIntent: _ExtendSelectionRightByWordTextAction(),
ExtendSelectionRightByLineTextIntent: _ExtendSelectionRightByLineTextAction(),
ExtendSelectionRightTextIntent: _ExtendSelectionRightTextAction(),
......@@ -118,6 +120,13 @@ class _ExtendSelectionLeftByLineTextAction extends TextEditingAction<ExtendSelec
}
}
class _ExtendSelectionLeftByWordAndStopAtReversalTextAction extends TextEditingAction<ExtendSelectionLeftByWordAndStopAtReversalTextIntent> {
@override
Object? invoke(ExtendSelectionLeftByWordAndStopAtReversalTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionLeftByWord(SelectionChangedCause.keyboard, false, true);
}
}
class _ExtendSelectionLeftByWordTextAction extends TextEditingAction<ExtendSelectionLeftByWordTextIntent> {
@override
Object? invoke(ExtendSelectionLeftByWordTextIntent intent, [BuildContext? context]) {
......@@ -139,6 +148,13 @@ class _ExtendSelectionRightByLineTextAction extends TextEditingAction<ExtendSele
}
}
class _ExtendSelectionRightByWordAndStopAtReversalTextAction extends TextEditingAction<ExtendSelectionRightByWordAndStopAtReversalTextIntent> {
@override
Object? invoke(ExtendSelectionRightByWordAndStopAtReversalTextIntent intent, [BuildContext? context]) {
textEditingActionTarget!.renderEditable.extendSelectionRightByWord(SelectionChangedCause.keyboard, false, true);
}
}
class _ExtendSelectionRightByWordTextAction extends TextEditingAction<ExtendSelectionRightByWordTextIntent> {
@override
Object? invoke(ExtendSelectionRightByWordTextIntent intent, [BuildContext? context]) {
......
......@@ -314,8 +314,8 @@ class DefaultTextEditingShortcuts extends Shortcuts {
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowRight): const MoveSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.arrowUp): const MoveSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowDown): const ExtendSelectionRightByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowLeft): const ExtendSelectionLeftByWordAndStopAtReversalTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowRight): const ExtendSelectionRightByWordAndStopAtReversalTextIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.shift, LogicalKeyboardKey.arrowUp): const ExtendSelectionLeftByLineTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowDown): const MoveSelectionDownTextIntent(),
LogicalKeySet(LogicalKeyboardKey.arrowLeft): const MoveSelectionLeftTextIntent(),
......
......@@ -69,6 +69,16 @@ class ExtendSelectionLeftByLineTextIntent extends Intent{
const ExtendSelectionLeftByLineTextIntent();
}
/// An [Intent] to extend the selection left past the nearest word, collapsing
/// the selection if the order of [TextSelection.extentOffset] and
/// [TextSelection.baseOffset] would reverse.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionLeftByWordAndStopAtReversalTextIntent extends Intent{
/// Creates an instance of ExtendSelectionLeftByWordAndStopAtReversalTextIntent.
const ExtendSelectionLeftByWordAndStopAtReversalTextIntent();
}
/// An [Intent] to extend the selection left past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
......@@ -95,6 +105,16 @@ class ExtendSelectionRightByLineTextIntent extends Intent{
const ExtendSelectionRightByLineTextIntent();
}
/// An [Intent] to extend the selection right past the nearest word, collapsing
/// the selection if the order of [TextSelection.extentOffset] and
/// [TextSelection.baseOffset] would reverse.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
class ExtendSelectionRightByWordAndStopAtReversalTextIntent extends Intent{
/// Creates an instance of ExtendSelectionRightByWordAndStopAtReversalTextIntent.
const ExtendSelectionRightByWordAndStopAtReversalTextIntent();
}
/// An [Intent] to extend the selection right past the nearest word.
///
/// {@macro flutter.widgets.TextEditingIntents.seeAlso}
......
......@@ -7224,6 +7224,147 @@ void main() {
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb);
testWidgets('navigating by word', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: 'word word word');
// word wo|rd| word
controller.selection = const TextSelection(
baseOffset: 7,
extentOffset: 9,
affinity: TextAffinity.upstream,
);
await tester.pumpWidget(MaterialApp(
home: Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: 400,
child: EditableText(
maxLines: 10,
controller: controller,
autofocus: true,
focusNode: focusNode,
style: Typography.material2018(platform: TargetPlatform.android).black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
keyboardType: TextInputType.text,
),
),
),
));
await tester.pump(); // Wait for autofocus to take effect.
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 9);
final String targetPlatform = defaultTargetPlatform.toString();
final String platform = targetPlatform.substring(targetPlatform.indexOf('.') + 1).toLowerCase();
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowRight],
shift: true,
wordModifier: true,
platform: platform,
);
await tester.pump();
// word wo|rd word|
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 14);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft],
shift: true,
wordModifier: true,
platform: platform,
);
// word wo|rd |word
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 10);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft],
shift: true,
wordModifier: true,
platform: platform,
);
if (platform == 'macos') {
// word wo|rd word
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 7);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft],
shift: true,
wordModifier: true,
platform: platform,
);
}
// word |wo|rd word
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 5);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowLeft],
shift: true,
wordModifier: true,
platform: platform,
);
// |word wo|rd word
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 0);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowRight],
shift: true,
wordModifier: true,
platform: platform,
);
// word| wo|rd word
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 4);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowRight],
shift: true,
wordModifier: true,
platform: platform,
);
if (platform == 'macos') {
// word wo|rd word
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 7);
await sendKeys(
tester,
<LogicalKeyboardKey>[LogicalKeyboardKey.arrowRight],
shift: true,
wordModifier: true,
platform: platform,
);
}
// word wo|rd| word
expect(controller.selection.isCollapsed, false);
expect(controller.selection.baseOffset, 7);
expect(controller.selection.extentOffset, 9);
// On web, using keyboard for selection is handled by the browser.
}, skip: kIsWeb, variant: TargetPlatformVariant.all());
testWidgets('can change behavior by overriding text editing actions', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(text: testText);
controller.selection = const TextSelection(
......
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