Skip to content

Commit c42b36f

Browse files
authored
Windows/Linux keyboard shortcuts at a wordwrap (#96323)
1 parent ac708f1 commit c42b36f

File tree

4 files changed

+1176
-162
lines changed

4 files changed

+1176
-162
lines changed

packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
317317
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
318318
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
319319

320-
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
321-
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
320+
const SingleActivator(LogicalKeyboardKey.home): const ScrollToDocumentBoundaryIntent(forward: false),
321+
const SingleActivator(LogicalKeyboardKey.end): const ScrollToDocumentBoundaryIntent(forward: true),
322+
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
323+
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
322324

323325
const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
324326
const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
@@ -349,10 +351,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
349351
// * Meta + backspace
350352
static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
351353
..._commonShortcuts,
352-
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
353-
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
354-
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
355-
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
354+
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true, continuesAtWrap: true),
355+
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true, continuesAtWrap: true),
356+
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, continuesAtWrap: true),
357+
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false, continuesAtWrap: true),
356358
const SingleActivator(LogicalKeyboardKey.home, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
357359
const SingleActivator(LogicalKeyboardKey.end, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
358360
const SingleActivator(LogicalKeyboardKey.home, shift: true, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),

packages/flutter/lib/src/widgets/editable_text.dart

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3069,7 +3069,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
30693069
}
30703070
late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>(onInvoke: _replaceText);
30713071

3072+
// Scrolls either to the beginning or end of the document depending on the
3073+
// intent's `forward` parameter.
3074+
void _scrollToDocumentBoundary(ScrollToDocumentBoundaryIntent intent) {
3075+
if (intent.forward) {
3076+
bringIntoView(TextPosition(offset: _value.text.length));
3077+
} else {
3078+
bringIntoView(const TextPosition(offset: 0));
3079+
}
3080+
}
3081+
30723082
void _updateSelection(UpdateSelectionIntent intent) {
3083+
bringIntoView(intent.newSelection.extent);
30733084
userUpdateTextEditingValue(
30743085
intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
30753086
intent.cause,
@@ -3079,28 +3090,38 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
30793090

30803091
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
30813092

3082-
void _expandSelection(ExpandSelectionToLineBreakIntent intent) {
3093+
void _expandSelectionToDocumentBoundary(ExpandSelectionToDocumentBoundaryIntent intent) {
3094+
final _TextBoundary textBoundary = _documentBoundary(intent);
3095+
_expandSelection(intent.forward, textBoundary, true);
3096+
}
3097+
3098+
void _expandSelectionToLinebreak(ExpandSelectionToLineBreakIntent intent) {
30833099
final _TextBoundary textBoundary = _linebreak(intent);
3100+
_expandSelection(intent.forward, textBoundary);
3101+
}
3102+
3103+
void _expandSelection(bool forward, _TextBoundary textBoundary, [bool extentAtIndex = false]) {
30843104
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
30853105
if (!textBoundarySelection.isValid) {
30863106
return;
30873107
}
30883108

30893109
final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset;
3090-
final bool towardsExtent = intent.forward == inOrder;
3110+
final bool towardsExtent = forward == inOrder;
30913111
final TextPosition position = towardsExtent
30923112
? textBoundarySelection.extent
30933113
: textBoundarySelection.base;
30943114

3095-
final TextPosition newExtent = intent.forward
3115+
final TextPosition newExtent = forward
30963116
? textBoundary.getTrailingTextBoundaryAt(position)
30973117
: textBoundary.getLeadingTextBoundaryAt(position);
30983118

3099-
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed);
3119+
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed || extentAtIndex);
31003120
userUpdateTextEditingValue(
31013121
_value.copyWith(selection: newSelection),
31023122
SelectionChangedCause.keyboard,
31033123
);
3124+
bringIntoView(newSelection.extent);
31043125
}
31053126

31063127
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
@@ -3118,10 +3139,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
31183139
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary,)),
31193140
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
31203141
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
3121-
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelection)),
3142+
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
3143+
ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _expandSelectionToDocumentBoundary)),
31223144
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction),
31233145
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
31243146
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
3147+
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
31253148

31263149
// Copy Paste
31273150
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
@@ -3763,7 +3786,10 @@ class _WordBoundary extends _TextBoundary {
37633786
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
37643787
// text-affinity-aware.
37653788
class _LineBreak extends _TextBoundary {
3766-
const _LineBreak(this.textLayout, this.textEditingValue);
3789+
const _LineBreak(
3790+
this.textLayout,
3791+
this.textEditingValue,
3792+
);
37673793

37683794
final TextLayoutMetrics textLayout;
37693795

@@ -3776,6 +3802,7 @@ class _LineBreak extends _TextBoundary {
37763802
offset: textLayout.getLineAtOffset(position).start,
37773803
);
37783804
}
3805+
37793806
@override
37803807
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
37813808
return TextPosition(
@@ -3945,12 +3972,39 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
39453972
}
39463973

39473974
class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
3948-
_UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent);
3975+
_UpdateTextSelectionAction(
3976+
this.state,
3977+
this.ignoreNonCollapsedSelection,
3978+
this.getTextBoundariesForIntent,
3979+
);
39493980

39503981
final EditableTextState state;
39513982
final bool ignoreNonCollapsedSelection;
39523983
final _TextBoundary Function(T intent) getTextBoundariesForIntent;
39533984

3985+
static const int NEWLINE_CODE_UNIT = 10;
3986+
3987+
// Returns true iff the given position is at a wordwrap boundary in the
3988+
// upstream position.
3989+
bool _isAtWordwrapUpstream(TextPosition position) {
3990+
final TextPosition end = TextPosition(
3991+
offset: state.renderEditable.getLineAtOffset(position).end,
3992+
affinity: TextAffinity.upstream,
3993+
);
3994+
return end == position && end.offset != state.textEditingValue.text.length
3995+
&& state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT;
3996+
}
3997+
3998+
// Returns true iff the given position at a wordwrap boundary in the
3999+
// downstream position.
4000+
bool _isAtWordwrapDownstream(TextPosition position) {
4001+
final TextPosition start = TextPosition(
4002+
offset: state.renderEditable.getLineAtOffset(position).start,
4003+
);
4004+
return start == position && start.offset != 0
4005+
&& state.textEditingValue.text.codeUnitAt(position.offset - 1) != NEWLINE_CODE_UNIT;
4006+
}
4007+
39544008
@override
39554009
Object? invoke(T intent, [BuildContext? context]) {
39564010
final TextSelection selection = state._value.selection;
@@ -3986,7 +4040,23 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
39864040
);
39874041
}
39884042

3989-
final TextPosition extent = textBoundarySelection.extent;
4043+
TextPosition extent = textBoundarySelection.extent;
4044+
4045+
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
4046+
// move it just to the other side of the wordwrap.
4047+
if (intent.continuesAtWrap) {
4048+
if (intent.forward && _isAtWordwrapUpstream(extent)) {
4049+
extent = TextPosition(
4050+
offset: extent.offset,
4051+
);
4052+
} else if (!intent.forward && _isAtWordwrapDownstream(extent)) {
4053+
extent = TextPosition(
4054+
offset: extent.offset,
4055+
affinity: TextAffinity.upstream,
4056+
);
4057+
}
4058+
}
4059+
39904060
final TextPosition newExtent = intent.forward
39914061
? textBoundary.getTrailingTextBoundaryAt(extent)
39924062
: textBoundary.getLeadingTextBoundaryAt(extent);

packages/flutter/lib/src/widgets/text_editing_intents.dart

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ class DoNothingAndStopPropagationTextIntent extends Intent {
2020
/// direction of the current caret location.
2121
abstract class DirectionalTextEditingIntent extends Intent {
2222
/// Creates a [DirectionalTextEditingIntent].
23-
const DirectionalTextEditingIntent(this.forward);
23+
const DirectionalTextEditingIntent(
24+
this.forward,
25+
);
2426

2527
/// Whether the input field, if applicable, should perform the text editing
2628
/// operation from the current caret location towards the end of the document.
@@ -65,7 +67,10 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
6567
const DirectionalCaretMovementIntent(
6668
bool forward,
6769
this.collapseSelection,
68-
[this.collapseAtReversal = false]
70+
[
71+
this.collapseAtReversal = false,
72+
this.continuesAtWrap = false,
73+
]
6974
) : assert(!collapseSelection || !collapseAtReversal),
7075
super(forward);
7176

@@ -90,6 +95,14 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
9095
///
9196
/// Cannot be true when collapseSelection is true.
9297
final bool collapseAtReversal;
98+
99+
/// Whether or not to continue to the next line at a wordwrap.
100+
///
101+
/// If true, when an [Intent] to go to the beginning/end of a wordwrapped line
102+
/// is received and the selection is already at the beginning/end of the line,
103+
/// then the selection will be moved to the next/previous line. If false, the
104+
/// selection will remain at the wordwrap.
105+
final bool continuesAtWrap;
93106
}
94107

95108
/// Extends, or moves the current selection from the current
@@ -132,6 +145,23 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
132145
}) : super(forward);
133146
}
134147

148+
/// Expands the current selection to the document boundary in the direction
149+
/// given by [forward].
150+
///
151+
/// Unlike [ExpandSelectionToLineBreakIntent], the extent will be moved, which
152+
/// matches the behavior on MacOS.
153+
///
154+
/// See also:
155+
///
156+
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
157+
/// moves the extent.
158+
class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
159+
/// Creates an [ExpandSelectionToDocumentBoundaryIntent].
160+
const ExpandSelectionToDocumentBoundaryIntent({
161+
required bool forward,
162+
}) : super(forward);
163+
}
164+
135165
/// Expands the current selection to the closest line break in the direction
136166
/// given by [forward].
137167
///
@@ -165,8 +195,9 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
165195
required bool forward,
166196
required bool collapseSelection,
167197
bool collapseAtReversal = false,
198+
bool continuesAtWrap = false,
168199
}) : assert(!collapseSelection || !collapseAtReversal),
169-
super(forward, collapseSelection, collapseAtReversal);
200+
super(forward, collapseSelection, collapseAtReversal, continuesAtWrap);
170201
}
171202

172203
/// Extends, or moves the current selection from the current
@@ -182,6 +213,11 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
182213

183214
/// Extends, or moves the current selection from the current
184215
/// [TextSelection.extent] position to the start or the end of the document.
216+
///
217+
/// See also:
218+
///
219+
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
220+
/// increases the size of the selection.
185221
class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
186222
/// Creates an [ExtendSelectionToDocumentBoundaryIntent].
187223
const ExtendSelectionToDocumentBoundaryIntent({
@@ -190,6 +226,15 @@ class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIn
190226
}) : super(forward, collapseSelection);
191227
}
192228

229+
/// Scrolls to the beginning or end of the document depending on the [forward]
230+
/// parameter.
231+
class ScrollToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
232+
/// Creates a [ScrollToDocumentBoundaryIntent].
233+
const ScrollToDocumentBoundaryIntent({
234+
required bool forward,
235+
}) : super(forward);
236+
}
237+
193238
/// An [Intent] to select everything in the field.
194239
class SelectAllTextIntent extends Intent {
195240
/// Creates an instance of [SelectAllTextIntent].

0 commit comments

Comments
 (0)