Skip to content

Commit 563e0a4

Browse files
authored
Page Up / Page Down in text fields (#107602)
1 parent b375b4a commit 563e0a4

File tree

7 files changed

+221
-21
lines changed

7 files changed

+221
-21
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@ Elsabe Ros <[email protected]>
9898
Nguyễn Phúc Lợi <[email protected]>
9999
Jingyi Chen <[email protected]>
100100
Junhua Lin <[email protected]>
101+
Tomasz Gucio <[email protected]>

packages/flutter/lib/src/rendering/editable.dart

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,13 @@ class TextSelectionPoint {
108108
/// false. Similarly the [moveNext] method moves the caret to the next line, and
109109
/// returns false if the caret is already on the last line.
110110
///
111+
/// The [moveByOffset] method takes a pixel offset from the current position to move
112+
/// the caret up or down.
113+
///
111114
/// If the underlying paragraph's layout changes, [isValid] becomes false and
112115
/// the [VerticalCaretMovementRun] must not be used. The [isValid] property must
113-
/// be checked before calling [movePrevious] and [moveNext], or accessing
114-
/// [current].
116+
/// be checked before calling [movePrevious], [moveNext] and [moveByOffset],
117+
/// or accessing [current].
115118
class VerticalCaretMovementRun extends Iterator<TextPosition> {
116119
VerticalCaretMovementRun._(
117120
this._editable,
@@ -134,8 +137,8 @@ class VerticalCaretMovementRun extends Iterator<TextPosition> {
134137
/// A [VerticalCaretMovementRun] run is valid if the underlying text layout
135138
/// hasn't changed.
136139
///
137-
/// The [current] value and the [movePrevious] and [moveNext] methods must not
138-
/// be accessed when [isValid] is false.
140+
/// The [current] value and the [movePrevious], [moveNext] and [moveByOffset]
141+
/// methods must not be accessed when [isValid] is false.
139142
bool get isValid {
140143
if (!_isValid) {
141144
return false;
@@ -200,6 +203,30 @@ class VerticalCaretMovementRun extends Iterator<TextPosition> {
200203
_currentTextPosition = position.value;
201204
return true;
202205
}
206+
207+
/// Move forward or backward by a number of elements determined
208+
/// by pixel [offset].
209+
///
210+
/// If [offset] is negative, move backward; otherwise move forward.
211+
///
212+
/// Returns true and updates [current] if successful.
213+
bool moveByOffset(double offset) {
214+
final Offset initialOffset = _currentOffset;
215+
if (offset >= 0.0) {
216+
while (_currentOffset.dy < initialOffset.dy + offset) {
217+
if (!moveNext()) {
218+
break;
219+
}
220+
}
221+
} else {
222+
while (_currentOffset.dy > initialOffset.dy + offset) {
223+
if (!movePrevious()) {
224+
break;
225+
}
226+
}
227+
}
228+
return initialOffset != _currentOffset;
229+
}
203230
}
204231

205232
/// Displays some text in a scrollable container with a potentially blinking

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
171171
SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: pressShift): const DeleteToLineBreakIntent(forward: true),
172172
},
173173

174-
// Arrow: Move Selection.
174+
// Arrow: Move selection.
175175
const SingleActivator(LogicalKeyboardKey.arrowLeft): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
176176
const SingleActivator(LogicalKeyboardKey.arrowRight): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
177177
const SingleActivator(LogicalKeyboardKey.arrowUp): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
178178
const SingleActivator(LogicalKeyboardKey.arrowDown): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
179179

180-
// Shift + Arrow: Extend Selection.
180+
// Shift + Arrow: Extend selection.
181181
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
182182
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
183183
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
@@ -199,6 +199,14 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
199199
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): const ExtendSelectionToNextWordBoundaryIntent(forward: false, collapseSelection: false),
200200
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): const ExtendSelectionToNextWordBoundaryIntent(forward: true, collapseSelection: false),
201201

202+
// Page Up / Down: Move selection by page.
203+
const SingleActivator(LogicalKeyboardKey.pageUp): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
204+
const SingleActivator(LogicalKeyboardKey.pageDown): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
205+
206+
// Shift + Page Up / Down: Extend selection by page.
207+
const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
208+
const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
209+
202210
const SingleActivator(LogicalKeyboardKey.keyX, control: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
203211
const SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent.copy,
204212
const SingleActivator(LogicalKeyboardKey.keyV, control: true): const PasteTextIntent(SelectionChangedCause.keyboard),
@@ -258,10 +266,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
258266
// macOS document shortcuts: https://support.apple.com/en-us/HT201236.
259267
// The macOS shortcuts uses different word/line modifiers than most other
260268
// platforms.
261-
static final Map<ShortcutActivator, Intent> _macShortcuts = _iOSShortcuts;
262-
263-
// There is no complete documentation of iOS shortcuts.
264-
static final Map<ShortcutActivator, Intent> _iOSShortcuts = <ShortcutActivator, Intent>{
269+
static final Map<ShortcutActivator, Intent> _macShortcuts = <ShortcutActivator, Intent>{
265270
for (final bool pressShift in const <bool>[true, false])
266271
...<SingleActivator, Intent>{
267272
SingleActivator(LogicalKeyboardKey.backspace, shift: pressShift): const DeleteCharacterIntent(forward: false),
@@ -277,7 +282,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
277282
const SingleActivator(LogicalKeyboardKey.arrowUp): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
278283
const SingleActivator(LogicalKeyboardKey.arrowDown): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),
279284

280-
// Shift + Arrow: Extend Selection.
285+
// Shift + Arrow: Extend selection.
281286
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
282287
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
283288
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
@@ -310,6 +315,9 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
310315
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
311316
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
312317

318+
const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
319+
const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
320+
313321
const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
314322
const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
315323
const SingleActivator(LogicalKeyboardKey.keyV, meta: true): const PasteTextIntent(SelectionChangedCause.keyboard),
@@ -335,6 +343,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
335343
// * Control + shift? + Z
336344
};
337345

346+
// There is no complete documentation of iOS shortcuts: use macOS ones.
347+
static final Map<ShortcutActivator, Intent> _iOSShortcuts = _macShortcuts;
338348

339349
// The following key combinations have no effect on text editing on this
340350
// platform:
@@ -350,6 +360,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
350360
// * Meta + backspace
351361
static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
352362
..._commonShortcuts,
363+
const SingleActivator(LogicalKeyboardKey.pageUp): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
364+
const SingleActivator(LogicalKeyboardKey.pageDown): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
353365
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true, continuesAtWrap: true),
354366
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true, continuesAtWrap: true),
355367
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, continuesAtWrap: true),
@@ -385,7 +397,6 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
385397
const SingleActivator(LogicalKeyboardKey.keyA, meta: true): const DoNothingAndStopPropagationTextIntent(),
386398
};
387399

388-
389400
static const Map<ShortcutActivator, Intent> _commonDisablingTextShortcuts = <ShortcutActivator, Intent>{
390401
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): DoNothingAndStopPropagationTextIntent(),
391402
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): DoNothingAndStopPropagationTextIntent(),
@@ -407,6 +418,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
407418
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): DoNothingAndStopPropagationTextIntent(),
408419
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): DoNothingAndStopPropagationTextIntent(),
409420
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): DoNothingAndStopPropagationTextIntent(),
421+
SingleActivator(LogicalKeyboardKey.pageUp, shift: true): DoNothingAndStopPropagationTextIntent(),
422+
SingleActivator(LogicalKeyboardKey.pageDown, shift: true): DoNothingAndStopPropagationTextIntent(),
410423
SingleActivator(LogicalKeyboardKey.end, shift: true): DoNothingAndStopPropagationTextIntent(),
411424
SingleActivator(LogicalKeyboardKey.home, shift: true): DoNothingAndStopPropagationTextIntent(),
412425
SingleActivator(LogicalKeyboardKey.arrowDown): DoNothingAndStopPropagationTextIntent(),
@@ -417,6 +430,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
417430
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): DoNothingAndStopPropagationTextIntent(),
418431
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): DoNothingAndStopPropagationTextIntent(),
419432
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): DoNothingAndStopPropagationTextIntent(),
433+
SingleActivator(LogicalKeyboardKey.pageUp): DoNothingAndStopPropagationTextIntent(),
434+
SingleActivator(LogicalKeyboardKey.pageDown): DoNothingAndStopPropagationTextIntent(),
420435
SingleActivator(LogicalKeyboardKey.end): DoNothingAndStopPropagationTextIntent(),
421436
SingleActivator(LogicalKeyboardKey.home): DoNothingAndStopPropagationTextIntent(),
422437
SingleActivator(LogicalKeyboardKey.end, control: true): DoNothingAndStopPropagationTextIntent(),
@@ -545,8 +560,8 @@ Intent? intentForMacOSSelector(String selectorName) {
545560
// TODO(knopp): Page Up/Down intents are missing (https://github.com/flutter/flutter/pull/105497)
546561
'scrollPageUp:': ScrollToDocumentBoundaryIntent(forward: false),
547562
'scrollPageDown:': ScrollToDocumentBoundaryIntent(forward: true),
548-
'pageUpAndModifySelection': ExpandSelectionToDocumentBoundaryIntent(forward: false),
549-
'pageDownAndModifySelection:': ExpandSelectionToDocumentBoundaryIntent(forward: true),
563+
'pageUpAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
564+
'pageDownAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
550565

551566
// Escape key when there's no IME selection popup.
552567
'cancelOperation:': DismissIntent(),

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ class _DiscreteKeyFrameSimulation extends Simulation {
479479
/// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: true`) | Collapses the selection to the word boundary before/after the selection's [TextSelection.extent] position, or [TextSelection.base], whichever is closest in the given direction | Moves the caret to the previous/next word boundary. |
480480
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the line at the selection's [TextSelection.extent] position | Moves the caret to the start/end of the current line .|
481481
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent line | Moves the caret to the closest position on the previous/next adjacent line. |
482+
/// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: true`) | Collapses the selection to the position closest to the selection's [TextSelection.extent], on the previous/next adjacent page | Moves the caret to the closest position on the previous/next adjacent page. |
482483
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. |
483484
///
484485
/// #### Intents for Extending the Selection
@@ -490,6 +491,7 @@ class _DiscreteKeyFrameSimulation extends Simulation {
490491
/// | [ExtendSelectionToNextWordBoundaryOrCaretLocationIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the previous/next word boundary, or [TextSelection.base] whichever is closest in the given direction | Moves the selection's [TextSelection.extent] to the previous/next word boundary. |
491492
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line |
492493
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line |
494+
/// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent page |
493495
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document |
494496
/// | [SelectAllTextIntent] | Selects the entire document |
495497
///
@@ -3106,7 +3108,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
31063108
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
31073109
// to avoid this setState().
31083110
setState(() { /* We use widget.controller.value in build(). */ });
3109-
_adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges();
3111+
_verticalSelectionUpdateAction.stopCurrentVerticalRunIfSelectionChanges();
31103112
}
31113113

31123114
void _handleFocusChanged() {
@@ -3589,7 +3591,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
35893591
}
35903592
late final Action<UpdateSelectionIntent> _updateSelectionAction = CallbackAction<UpdateSelectionIntent>(onInvoke: _updateSelection);
35913593

3592-
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
3594+
late final _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent> _verticalSelectionUpdateAction =
3595+
_UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent>(this);
35933596

35943597
void _expandSelectionToDocumentBoundary(ExpandSelectionToDocumentBoundaryIntent intent) {
35953598
final TextBoundary textBoundary = _documentBoundary(intent);
@@ -3717,7 +3720,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
37173720
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
37183721
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
37193722
ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _expandSelectionToDocumentBoundary)),
3720-
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction),
3723+
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_verticalSelectionUpdateAction),
3724+
ExtendSelectionVerticallyToAdjacentPageIntent: _makeOverridable(_verticalSelectionUpdateAction),
37213725
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
37223726
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
37233727
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
@@ -4604,8 +4608,8 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
46044608
bool get isActionEnabled => state.widget.selectionEnabled && state._value.selection.isValid;
46054609
}
46064610

4607-
class _UpdateTextSelectionToAdjacentLineAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
4608-
_UpdateTextSelectionToAdjacentLineAction(this.state);
4611+
class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
4612+
_UpdateTextSelectionVerticallyAction(this.state);
46094613

46104614
final EditableTextState state;
46114615

@@ -4647,10 +4651,12 @@ class _UpdateTextSelectionToAdjacentLineAction<T extends DirectionalCaretMovemen
46474651
final VerticalCaretMovementRun currentRun = _verticalMovementRun
46484652
?? state.renderEditable.startVerticalCaretMovement(state.renderEditable.selection!.extent);
46494653

4650-
final bool shouldMove = intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
4654+
final bool shouldMove = intent is ExtendSelectionVerticallyToAdjacentPageIntent
4655+
? currentRun.moveByOffset((intent.forward ? 1.0 : -1.0) * state.renderEditable.size.height)
4656+
: intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
46514657
final TextPosition newExtent = shouldMove
46524658
? currentRun.current
4653-
: (intent.forward ? TextPosition(offset: state._value.text.length) : const TextPosition(offset: 0));
4659+
: intent.forward ? TextPosition(offset: state._value.text.length) : const TextPosition(offset: 0);
46544660
final TextSelection newSelection = collapseSelection
46554661
? TextSelection.fromPosition(newExtent)
46564662
: value.selection.extendTo(newExtent);

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,17 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
210210
}) : super(forward, collapseSelection);
211211
}
212212

213+
/// Expands, or moves the current selection from the current
214+
/// [TextSelection.extent] position to the closest position on the adjacent
215+
/// page.
216+
class ExtendSelectionVerticallyToAdjacentPageIntent extends DirectionalCaretMovementIntent {
217+
/// Creates an [ExtendSelectionVerticallyToAdjacentPageIntent].
218+
const ExtendSelectionVerticallyToAdjacentPageIntent({
219+
required bool forward,
220+
required bool collapseSelection,
221+
}) : super(forward, collapseSelection);
222+
}
223+
213224
/// Extends, or moves the current selection from the current
214225
/// [TextSelection.extent] position to the start or the end of the document.
215226
///

0 commit comments

Comments
 (0)