Skip to content

Commit

Permalink
Page Up / Page Down in text fields (flutter#107602)
Browse files Browse the repository at this point in the history
  • Loading branch information
tgucio committed Oct 25, 2022
1 parent b375b4a commit 563e0a4
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 21 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ Elsabe Ros <hello@elsabe.dev>
Nguyễn Phúc Lợi <nploi1998@gmail.com>
Jingyi Chen <jingyichen@link.cuhk.edu.cn>
Junhua Lin <1075209054@qq.com>
Tomasz Gucio <tgucio@gmail.com>
35 changes: 31 additions & 4 deletions packages/flutter/lib/src/rendering/editable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,13 @@ class TextSelectionPoint {
/// false. Similarly the [moveNext] method moves the caret to the next line, and
/// returns false if the caret is already on the last line.
///
/// The [moveByOffset] method takes a pixel offset from the current position to move
/// the caret up or down.
///
/// If the underlying paragraph's layout changes, [isValid] becomes false and
/// the [VerticalCaretMovementRun] must not be used. The [isValid] property must
/// be checked before calling [movePrevious] and [moveNext], or accessing
/// [current].
/// be checked before calling [movePrevious], [moveNext] and [moveByOffset],
/// or accessing [current].
class VerticalCaretMovementRun extends Iterator<TextPosition> {
VerticalCaretMovementRun._(
this._editable,
Expand All @@ -134,8 +137,8 @@ class VerticalCaretMovementRun extends Iterator<TextPosition> {
/// A [VerticalCaretMovementRun] run is valid if the underlying text layout
/// hasn't changed.
///
/// The [current] value and the [movePrevious] and [moveNext] methods must not
/// be accessed when [isValid] is false.
/// The [current] value and the [movePrevious], [moveNext] and [moveByOffset]
/// methods must not be accessed when [isValid] is false.
bool get isValid {
if (!_isValid) {
return false;
Expand Down Expand Up @@ -200,6 +203,30 @@ class VerticalCaretMovementRun extends Iterator<TextPosition> {
_currentTextPosition = position.value;
return true;
}

/// Move forward or backward by a number of elements determined
/// by pixel [offset].
///
/// If [offset] is negative, move backward; otherwise move forward.
///
/// Returns true and updates [current] if successful.
bool moveByOffset(double offset) {
final Offset initialOffset = _currentOffset;
if (offset >= 0.0) {
while (_currentOffset.dy < initialOffset.dy + offset) {
if (!moveNext()) {
break;
}
}
} else {
while (_currentOffset.dy > initialOffset.dy + offset) {
if (!movePrevious()) {
break;
}
}
}
return initialOffset != _currentOffset;
}
}

/// Displays some text in a scrollable container with a potentially blinking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,13 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: pressShift): const DeleteToLineBreakIntent(forward: true),
},

// Arrow: Move Selection.
// Arrow: Move selection.
const SingleActivator(LogicalKeyboardKey.arrowLeft): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowRight): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowUp): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowDown): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),

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

// Page Up / Down: Move selection by page.
const SingleActivator(LogicalKeyboardKey.pageUp): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.pageDown): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),

// Shift + Page Up / Down: Extend selection by page.
const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),

const SingleActivator(LogicalKeyboardKey.keyX, control: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
const SingleActivator(LogicalKeyboardKey.keyC, control: true): CopySelectionTextIntent.copy,
const SingleActivator(LogicalKeyboardKey.keyV, control: true): const PasteTextIntent(SelectionChangedCause.keyboard),
Expand Down Expand Up @@ -258,10 +266,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
// macOS document shortcuts: https://support.apple.com/en-us/HT201236.
// The macOS shortcuts uses different word/line modifiers than most other
// platforms.
static final Map<ShortcutActivator, Intent> _macShortcuts = _iOSShortcuts;

// There is no complete documentation of iOS shortcuts.
static final Map<ShortcutActivator, Intent> _iOSShortcuts = <ShortcutActivator, Intent>{
static final Map<ShortcutActivator, Intent> _macShortcuts = <ShortcutActivator, Intent>{
for (final bool pressShift in const <bool>[true, false])
...<SingleActivator, Intent>{
SingleActivator(LogicalKeyboardKey.backspace, shift: pressShift): const DeleteCharacterIntent(forward: false),
Expand All @@ -277,7 +282,7 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
const SingleActivator(LogicalKeyboardKey.arrowUp): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.arrowDown): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: true, collapseSelection: true),

// Shift + Arrow: Extend Selection.
// Shift + Arrow: Extend selection.
const SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): const ExtendSelectionByCharacterIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): const ExtendSelectionByCharacterIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): const ExtendSelectionVerticallyToAdjacentLineIntent(forward: false, collapseSelection: false),
Expand Down Expand Up @@ -310,6 +315,9 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),

const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),

const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
const SingleActivator(LogicalKeyboardKey.keyV, meta: true): const PasteTextIntent(SelectionChangedCause.keyboard),
Expand All @@ -335,6 +343,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
// * Control + shift? + Z
};

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

// The following key combinations have no effect on text editing on this
// platform:
Expand All @@ -350,6 +360,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
// * Meta + backspace
static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
..._commonShortcuts,
const SingleActivator(LogicalKeyboardKey.pageUp): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.pageDown): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, continuesAtWrap: true),
Expand Down Expand Up @@ -385,7 +397,6 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
const SingleActivator(LogicalKeyboardKey.keyA, meta: true): const DoNothingAndStopPropagationTextIntent(),
};


static const Map<ShortcutActivator, Intent> _commonDisablingTextShortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.arrowDown, alt: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowLeft, alt: true): DoNothingAndStopPropagationTextIntent(),
Expand All @@ -407,6 +418,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowUp, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.pageUp, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.pageDown, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.end, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.home, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowDown): DoNothingAndStopPropagationTextIntent(),
Expand All @@ -417,6 +430,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
SingleActivator(LogicalKeyboardKey.arrowRight, control: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowLeft, shift: true, control: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.arrowRight, shift: true, control: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.pageUp): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.pageDown): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.end): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.home): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.end, control: true): DoNothingAndStopPropagationTextIntent(),
Expand Down Expand Up @@ -545,8 +560,8 @@ Intent? intentForMacOSSelector(String selectorName) {
// TODO(knopp): Page Up/Down intents are missing (https://github.com/flutter/flutter/pull/105497)
'scrollPageUp:': ScrollToDocumentBoundaryIntent(forward: false),
'scrollPageDown:': ScrollToDocumentBoundaryIntent(forward: true),
'pageUpAndModifySelection': ExpandSelectionToDocumentBoundaryIntent(forward: false),
'pageDownAndModifySelection:': ExpandSelectionToDocumentBoundaryIntent(forward: true),
'pageUpAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
'pageDownAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),

// Escape key when there's no IME selection popup.
'cancelOperation:': DismissIntent(),
Expand Down
20 changes: 13 additions & 7 deletions packages/flutter/lib/src/widgets/editable_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ class _DiscreteKeyFrameSimulation extends Simulation {
/// | [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. |
/// | [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 .|
/// | [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. |
/// | [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. |
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: true`) | Collapses the selection to the start/end of the document | Moves the caret to the start/end of the document. |
///
/// #### Intents for Extending the Selection
Expand All @@ -490,6 +491,7 @@ class _DiscreteKeyFrameSimulation extends Simulation {
/// | [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. |
/// | [ExtendSelectionToLineBreakIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the line |
/// | [ExtendSelectionVerticallyToAdjacentLineIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent line |
/// | [ExtendSelectionVerticallyToAdjacentPageIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the closest position on the previous/next adjacent page |
/// | [ExtendSelectionToDocumentBoundaryIntent](`collapseSelection: false`) | Moves the selection's [TextSelection.extent] to the start/end of the document |
/// | [SelectAllTextIntent] | Selects the entire document |
///
Expand Down Expand Up @@ -3106,7 +3108,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
// to avoid this setState().
setState(() { /* We use widget.controller.value in build(). */ });
_adjacentLineAction.stopCurrentVerticalRunIfSelectionChanges();
_verticalSelectionUpdateAction.stopCurrentVerticalRunIfSelectionChanges();
}

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

late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
late final _UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent> _verticalSelectionUpdateAction =
_UpdateTextSelectionVerticallyAction<DirectionalCaretMovementIntent>(this);

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

class _UpdateTextSelectionToAdjacentLineAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
_UpdateTextSelectionToAdjacentLineAction(this.state);
class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
_UpdateTextSelectionVerticallyAction(this.state);

final EditableTextState state;

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

final bool shouldMove = intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
final bool shouldMove = intent is ExtendSelectionVerticallyToAdjacentPageIntent
? currentRun.moveByOffset((intent.forward ? 1.0 : -1.0) * state.renderEditable.size.height)
: intent.forward ? currentRun.moveNext() : currentRun.movePrevious();
final TextPosition newExtent = shouldMove
? currentRun.current
: (intent.forward ? TextPosition(offset: state._value.text.length) : const TextPosition(offset: 0));
: intent.forward ? TextPosition(offset: state._value.text.length) : const TextPosition(offset: 0);
final TextSelection newSelection = collapseSelection
? TextSelection.fromPosition(newExtent)
: value.selection.extendTo(newExtent);
Expand Down
11 changes: 11 additions & 0 deletions packages/flutter/lib/src/widgets/text_editing_intents.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
}) : super(forward, collapseSelection);
}

/// Expands, or moves the current selection from the current
/// [TextSelection.extent] position to the closest position on the adjacent
/// page.
class ExtendSelectionVerticallyToAdjacentPageIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionVerticallyToAdjacentPageIntent].
const ExtendSelectionVerticallyToAdjacentPageIntent({
required bool forward,
required bool collapseSelection,
}) : super(forward, collapseSelection);
}

/// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the start or the end of the document.
///
Expand Down

0 comments on commit 563e0a4

Please sign in to comment.