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 {
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.home, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.home): const ScrollToDocumentBoundaryIntent(forward: 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.keyC, meta: true): CopySelectionTextIntent.copy,
......@@ -349,10 +351,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + backspace
static final Map<ShortcutActivator, Intent> _windowsShortcuts = <ShortcutActivator, Intent>{
..._commonShortcuts,
const SingleActivator(LogicalKeyboardKey.home): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.end): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: true),
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExtendSelectionToLineBreakIntent(forward: false, collapseSelection: false),
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExtendSelectionToLineBreakIntent(forward: true, collapseSelection: false),
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),
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.end, control: true): const ExtendSelectionToDocumentBoundaryIntent(forward: true, collapseSelection: true),
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
}
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) {
bringIntoView(intent.newSelection.extent);
userUpdateTextEditingValue(
intent.currentTextEditingValue.copyWith(selection: intent.newSelection),
intent.cause,
......@@ -3079,28 +3090,38 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
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);
_expandSelection(intent.forward, textBoundary);
}
void _expandSelection(bool forward, _TextBoundary textBoundary, [bool extentAtIndex = false]) {
final TextSelection textBoundarySelection = textBoundary.textEditingValue.selection;
if (!textBoundarySelection.isValid) {
return;
}
final bool inOrder = textBoundarySelection.baseOffset <= textBoundarySelection.extentOffset;
final bool towardsExtent = intent.forward == inOrder;
final bool towardsExtent = forward == inOrder;
final TextPosition position = towardsExtent
? textBoundarySelection.extent
: textBoundarySelection.base;
final TextPosition newExtent = intent.forward
final TextPosition newExtent = forward
? textBoundary.getTrailingTextBoundaryAt(position)
: textBoundary.getLeadingTextBoundaryAt(position);
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed);
final TextSelection newSelection = textBoundarySelection.expandTo(newExtent, textBoundarySelection.isCollapsed || extentAtIndex);
userUpdateTextEditingValue(
_value.copyWith(selection: newSelection),
SelectionChangedCause.keyboard,
);
bringIntoView(newSelection.extent);
}
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
......@@ -3118,10 +3139,12 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary,)),
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
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),
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
// Copy Paste
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
......@@ -3763,7 +3786,10 @@ class _WordBoundary extends _TextBoundary {
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware.
class _LineBreak extends _TextBoundary {
const _LineBreak(this.textLayout, this.textEditingValue);
const _LineBreak(
this.textLayout,
this.textEditingValue,
);
final TextLayoutMetrics textLayout;
......@@ -3776,6 +3802,7 @@ class _LineBreak extends _TextBoundary {
offset: textLayout.getLineAtOffset(position).start,
);
}
@override
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
return TextPosition(
......@@ -3945,12 +3972,39 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
}
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 bool ignoreNonCollapsedSelection;
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
Object? invoke(T intent, [BuildContext? context]) {
final TextSelection selection = state._value.selection;
......@@ -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
? textBoundary.getTrailingTextBoundaryAt(extent)
: textBoundary.getLeadingTextBoundaryAt(extent);
......
......@@ -20,7 +20,9 @@ class DoNothingAndStopPropagationTextIntent extends Intent {
/// direction of the current caret location.
abstract class DirectionalTextEditingIntent extends Intent {
/// Creates a [DirectionalTextEditingIntent].
const DirectionalTextEditingIntent(this.forward);
const DirectionalTextEditingIntent(
this.forward,
);
/// Whether the input field, if applicable, should perform the text editing
/// operation from the current caret location towards the end of the document.
......@@ -65,7 +67,10 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
const DirectionalCaretMovementIntent(
bool forward,
this.collapseSelection,
[this.collapseAtReversal = false]
[
this.collapseAtReversal = false,
this.continuesAtWrap = false,
]
) : assert(!collapseSelection || !collapseAtReversal),
super(forward);
......@@ -90,6 +95,14 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
///
/// Cannot be true when collapseSelection is true.
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
......@@ -132,6 +145,23 @@ class ExtendSelectionToNextWordBoundaryOrCaretLocationIntent extends Directional
}) : 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
/// given by [forward].
///
......@@ -165,8 +195,9 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
required bool forward,
required bool collapseSelection,
bool collapseAtReversal = false,
bool continuesAtWrap = false,
}) : assert(!collapseSelection || !collapseAtReversal),
super(forward, collapseSelection, collapseAtReversal);
super(forward, collapseSelection, collapseAtReversal, continuesAtWrap);
}
/// Extends, or moves the current selection from the current
......@@ -182,6 +213,11 @@ class ExtendSelectionVerticallyToAdjacentLineIntent extends DirectionalCaretMove
/// Extends, or moves the current selection from the current
/// [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 {
/// Creates an [ExtendSelectionToDocumentBoundaryIntent].
const ExtendSelectionToDocumentBoundaryIntent({
......@@ -190,6 +226,15 @@ class ExtendSelectionToDocumentBoundaryIntent extends DirectionalCaretMovementIn
}) : 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.
class SelectAllTextIntent extends Intent {
/// 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