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

Windows/Linux keyboard shortcuts at a wordwrap (#96323)

parent ac708f1c
...@@ -317,8 +317,10 @@ class DefaultTextEditingShortcuts extends Shortcuts { ...@@ -317,8 +317,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false), const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false), const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, meta: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false), const SingleActivator(LogicalKeyboardKey.home): const ScrollToDocumentBoundaryIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false), const SingleActivator(LogicalKeyboardKey.end): const ScrollToDocumentBoundaryIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard), const SingleActivator(LogicalKeyboardKey.keyX, meta: true): const CopySelectionTextIntent.cut(SelectionChangedCause.keyboard),
const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy, const SingleActivator(LogicalKeyboardKey.keyC, meta: true): CopySelectionTextIntent.copy,
...@@ -349,10 +351,10 @@ class DefaultTextEditingShortcuts extends Shortcuts { ...@@ -349,10 +351,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + backspace // * Meta + backspace
static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{ static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
..._commonShortcuts, ..._commonShortcuts,
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true), const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: 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), const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false), const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false, continuesAtWrap: true),
const SingleActivator(LogicalKeyboardKey.home, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true), const SingleActivator(LogicalKeyboardKey.home, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.end, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true), const SingleActivator(LogicalKeyboardKey.end, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false), const SingleActivator(LogicalKeyboardKey.home, shift: true, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
......
...@@ -3069,7 +3069,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3069,7 +3069,18 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
} }
late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>(onInvoke: _replaceText); late final Action<ReplaceTextIntent> _replaceTextAction = CallbackAction<ReplaceTextIntent>(onInvoke: _replaceText);
// Scrolls either to the beginning or end of the document depending on the
// intent's `forward` parameter.
void _scrollToDocumentBoundary(ScrollToDocumentBoundaryIntent intent) {
if (intent.forward) {
bringIntoView(TextPosition(offset: _value.text.length));
} else {
bringIntoView(const TextPosition(offset: 0));
}
}
void _updateSelection(UpdateSelectionIntent intent) { void _updateSelection(UpdateSelectionIntent intent) {
bringIntoView(intent.newSelection.extent);
userUpdateTextEditingValue( userUpdateTextEditingValue(
intent.currentTextEditingValue.copyWith(selection: intent.newSelection), intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
intent.cause, intent.cause,
...@@ -3079,28 +3090,38 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3079,28 +3090,38 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this); late final _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent> _adjacentLineAction = _UpdateTextSelectionToAdjacentLineAction<ExtendSelectionVerticallyToAdjacentLineIntent>(this);
void _expandSelection(ExpandSelectionToLineBreakIntent intent) { void _expandSelectionToDocumentBoundary(ExpandSelectionToDocumentBoundaryIntent intent) {
final _TextBoundary textBoundary = _documentBoundary(intent);
_expandSelection(intent.forward, textBoundary, true);
}
void _expandSelectionToLinebreak(ExpandSelectionToLineBreakIntent intent) {
final _TextBoundary textBoundary = _linebreak(intent); final _TextBoundary textBoundary = _linebreak(intent);
_expandSelection(intent.forward, textBoundary);
}
void _expandSelection(bool forward, _TextBoundary textBoundary, [bool extentAtIndex = false]) {
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection; final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) { if (!textBoundarySelection.isValid) {
return; return;
} }
final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset; final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset;
final bool towardsExtent = intent.forward == inOrder; final bool towardsExtent = forward == inOrder;
final TextPosition position = towardsExtent final TextPosition position = towardsExtent
? textBoundarySelection.extent ? textBoundarySelection.extent
: textBoundarySelection.base; : textBoundarySelection.base;
final TextPosition newExtent = intent.forward final TextPosition newExtent = forward
? textBoundary.getTrailingTextBoundaryAt(position) ? textBoundary.getTrailingTextBoundaryAt(position)
: textBoundary.getLeadingTextBoundaryAt(position); : textBoundary.getLeadingTextBoundaryAt(position);
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed); final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed || extentAtIndex);
userUpdateTextEditingValue( userUpdateTextEditingValue(
_value.copyWith(selection: newSelection), _value.copyWith(selection: newSelection),
SelectionChangedCause.keyboard, SelectionChangedCause.keyboard,
); );
bringIntoView(newSelection.extent);
} }
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{ late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
...@@ -3118,10 +3139,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3118,10 +3139,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary,)), ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary,)),
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)), ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelection)), ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
ExpandSelectionToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ExpandSelectionToDocumentBoundaryIntent>(onInvoke: _expandSelectionToDocumentBoundary)),
ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction), ExtendSelectionVerticallyToAdjacentLineIntent: _makeOverridable(_adjacentLineAction),
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)), ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)), ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
// Copy Paste // Copy Paste
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)), SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
...@@ -3763,7 +3786,10 @@ class _WordBoundary extends _TextBoundary { ...@@ -3763,7 +3786,10 @@ class _WordBoundary extends _TextBoundary {
// interpreted as caret locations because [TextPainter.getLineAtOffset] is // interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware. // text-affinity-aware.
class _LineBreak extends _TextBoundary { class _LineBreak extends _TextBoundary {
const _LineBreak(this.textLayout, this.textEditingValue); const _LineBreak(
this.textLayout,
this.textEditingValue,
);
final TextLayoutMetrics textLayout; final TextLayoutMetrics textLayout;
...@@ -3776,6 +3802,7 @@ class _LineBreak extends _TextBoundary { ...@@ -3776,6 +3802,7 @@ class _LineBreak extends _TextBoundary {
offset: textLayout.getLineAtOffset(position).start, offset: textLayout.getLineAtOffset(position).start,
); );
} }
@override @override
TextPosition getTrailingTextBoundaryAt(TextPosition position) { TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition( return TextPosition(
...@@ -3945,12 +3972,39 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA ...@@ -3945,12 +3972,39 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
} }
class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> { class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> extends ContextAction<T> {
_UpdateTextSelectionAction(this.state, this.ignoreNonCollapsedSelection, this.getTextBoundariesForIntent); _UpdateTextSelectionAction(
this.state,
this.ignoreNonCollapsedSelection,
this.getTextBoundariesForIntent,
);
final EditableTextState state; final EditableTextState state;
final bool ignoreNonCollapsedSelection; final bool ignoreNonCollapsedSelection;
final _TextBoundary Function(T intent) getTextBoundariesForIntent; final _TextBoundary Function(T intent) getTextBoundariesForIntent;
static const int NEWLINE_CODE_UNIT = 10;
// Returns true iff the given position is at a wordwrap boundary in the
// upstream position.
bool _isAtWordwrapUpstream(TextPosition position) {
final TextPosition end = TextPosition(
offset: state.renderEditable.getLineAtOffset(position).end,
affinity: TextAffinity.upstream,
);
return end == position && end.offset != state.textEditingValue.text.length
&& state.textEditingValue.text.codeUnitAt(position.offset) != NEWLINE_CODE_UNIT;
}
// Returns true iff the given position at a wordwrap boundary in the
// downstream position.
bool _isAtWordwrapDownstream(TextPosition position) {
final TextPosition start = TextPosition(
offset: state.renderEditable.getLineAtOffset(position).start,
);
return start == position && start.offset != 0
&& state.textEditingValue.text.codeUnitAt(position.offset - 1) != NEWLINE_CODE_UNIT;
}
@override @override
Object? invoke(T intent, [BuildContext? context]) { Object? invoke(T intent, [BuildContext? context]) {
final TextSelection selection = state._value.selection; final TextSelection selection = state._value.selection;
...@@ -3986,7 +4040,23 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten ...@@ -3986,7 +4040,23 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
); );
} }
final TextPosition extent = textBoundarySelection.extent; TextPosition extent = textBoundarySelection.extent;
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
// move it just to the other side of the wordwrap.
if (intent.continuesAtWrap) {
if (intent.forward && _isAtWordwrapUpstream(extent)) {
extent = TextPosition(
offset: extent.offset,
);
} else if (!intent.forward && _isAtWordwrapDownstream(extent)) {
extent = TextPosition(
offset: extent.offset,
affinity: TextAffinity.upstream,
);
}
}
final TextPosition newExtent = intent.forward final TextPosition newExtent = intent.forward
? textBoundary.getTrailingTextBoundaryAt(extent) ? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent); : textBoundary.getLeadingTextBoundaryAt(extent);
......
...@@ -20,7 +20,9 @@ class DoNothingAndStopPropagationTextIntent extends Intent { ...@@ -20,7 +20,9 @@ class DoNothingAndStopPropagationTextIntent extends Intent {
/// direction of the current caret location. /// direction of the current caret location.
abstract class DirectionalTextEditingIntent extends Intent { abstract class DirectionalTextEditingIntent extends Intent {
/// Creates a [DirectionalTextEditingIntent]. /// Creates a [DirectionalTextEditingIntent].
const DirectionalTextEditingIntent(this.forward); const DirectionalTextEditingIntent(
this.forward,
);
/// Whether the input field, if applicable, should perform the text editing /// Whether the input field, if applicable, should perform the text editing
/// operation from the current caret location towards the end of the document. /// operation from the current caret location towards the end of the document.
...@@ -65,7 +67,10 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte ...@@ -65,7 +67,10 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
const DirectionalCaretMovementIntent( const DirectionalCaretMovementIntent(
bool forward, bool forward,
this.collapseSelection, this.collapseSelection,
[this.collapseAtReversal = false] [
this.collapseAtReversal = false,
this.continuesAtWrap = false,
]
) : assert(!collapseSelection || !collapseAtReversal), ) : assert(!collapseSelection || !collapseAtReversal),
super(forward); super(forward);
...@@ -90,6 +95,14 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte ...@@ -90,6 +95,14 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
/// ///
/// Cannot be true when collapseSelection is true. /// Cannot be true when collapseSelection is true.
final bool collapseAtReversal; final bool collapseAtReversal;
/// Whether or not to continue to the next line at a wordwrap.
///
/// If true, when an [Intent] to go to the beginning/end of a wordwrapped line
/// is received and the selection is already at the beginning/end of the line,
/// then the selection will be moved to the next/previous line. If false, the
/// selection will remain at the wordwrap.
final bool continuesAtWrap;
} }
/// Extends, or moves the current selection from the current /// Extends, or moves the current selection from the current
...@@ -132,6 +145,23 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional ...@@ -132,6 +145,23 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
}) : super(forward); }) : super(forward);
} }
/// Expands the current selection to the document boundary in the direction
/// given by [forward].
///
/// Unlike [ExpandSelectionToLineBreakIntent], the extent will be moved, which
/// matches the behavior on MacOS.
///
/// See also:
///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// moves the extent.
class ExpandSelectionToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
/// Creates an [ExpandSelectionToDocumentBoundaryIntent].
const ExpandSelectionToDocumentBoundaryIntent({
required bool forward,
}) : super(forward);
}
/// Expands the current selection to the closest line break in the direction /// Expands the current selection to the closest line break in the direction
/// given by [forward]. /// given by [forward].
/// ///
...@@ -165,8 +195,9 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent { ...@@ -165,8 +195,9 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
required bool forward, required bool forward,
required bool collapseSelection, required bool collapseSelection,
bool collapseAtReversal = false, bool collapseAtReversal = false,
bool continuesAtWrap = false,
}) : assert(!collapseSelection || !collapseAtReversal), }) : assert(!collapseSelection || !collapseAtReversal),
super(forward, collapseSelection, collapseAtReversal); super(forward, collapseSelection, collapseAtReversal, continuesAtWrap);
} }
/// Extends, or moves the current selection from the current /// Extends, or moves the current selection from the current
...@@ -182,6 +213,11 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove ...@@ -182,6 +213,11 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
/// Extends, or moves the current selection from the current /// Extends, or moves the current selection from the current
/// [TextSelection.extent] position to the start or the end of the document. /// [TextSelection.extent] position to the start or the end of the document.
///
/// See also:
///
/// [ExtendSelectionToDocumentBoundaryIntent], which is similar but always
/// increases the size of the selection.
class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent { class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIntent {
/// Creates an [ExtendSelectionToDocumentBoundaryIntent]. /// Creates an [ExtendSelectionToDocumentBoundaryIntent].
const ExtendSelectionToDocumentBoundaryIntent({ const ExtendSelectionToDocumentBoundaryIntent({
...@@ -190,6 +226,15 @@ class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIn ...@@ -190,6 +226,15 @@ class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIn
}) : super(forward, collapseSelection); }) : super(forward, collapseSelection);
} }
/// Scrolls to the beginning or end of the document depending on the [forward]
/// parameter.
class ScrollToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
/// Creates a [ScrollToDocumentBoundaryIntent].
const ScrollToDocumentBoundaryIntent({
required bool forward,
}) : super(forward);
}
/// An [Intent] to select everything in the field. /// An [Intent] to select everything in the field.
class SelectAllTextIntent extends Intent { class SelectAllTextIntent extends Intent {
/// Creates an instance of [SelectAllTextIntent]. /// Creates an instance of [SelectAllTextIntent].
......
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