Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
a1115b8b
Unverified
Commit
a1115b8b
authored
Nov 23, 2021
by
Justin McCandless
Committed by
GitHub
Nov 23, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Windows home/end shortcuts (#90840)
Support for Windows home/end keyboard shortcuts
parent
f62a1280
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
326 additions
and
1696 deletions
+326
-1696
default_text_editing_shortcuts.dart
...utter/lib/src/widgets/default_text_editing_shortcuts.dart
+27
-3
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+15
-0
text_editing_action_target.dart
...s/flutter/lib/src/widgets/text_editing_action_target.dart
+0
-1572
text_editing_intents.dart
packages/flutter/lib/src/widgets/text_editing_intents.dart
+19
-3
default_text_editing_actions_test.dart
...utter/test/widgets/default_text_editing_actions_test.dart
+0
-118
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+265
-0
No files found.
packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart
View file @
a1115b8b
...
...
@@ -256,6 +256,10 @@ class DefaultTextEditingShortcuts extends Shortcuts {
SingleActivator
(
LogicalKeyboardKey
.
end
,
shift:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
true
,
collapseSelection:
false
),
// The following key combinations have no effect on text editing on this
// platform:
// * Control + end
// * Control + home
// * Control + shift + end
// * Control + shift + home
// * Meta + X
// * Meta + C
// * Meta + V
...
...
@@ -301,8 +305,8 @@ class DefaultTextEditingShortcuts extends Shortcuts {
SingleActivator
(
LogicalKeyboardKey
.
arrowLeft
,
shift:
true
,
alt:
true
):
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent
(
forward:
false
),
SingleActivator
(
LogicalKeyboardKey
.
arrowRight
,
shift:
true
,
alt:
true
):
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent
(
forward:
true
),
SingleActivator
(
LogicalKeyboardKey
.
arrowUp
,
shift:
true
,
alt:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
false
,
collapseSelection:
false
),
SingleActivator
(
LogicalKeyboardKey
.
arrowDown
,
shift:
true
,
alt:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
true
,
collapseSelection:
false
),
SingleActivator
(
LogicalKeyboardKey
.
arrowUp
,
shift:
true
,
alt:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
false
,
collapseSelection:
false
,
collapseAtReversal:
true
),
SingleActivator
(
LogicalKeyboardKey
.
arrowDown
,
shift:
true
,
alt:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
true
,
collapseSelection:
false
,
collapseAtReversal:
true
),
SingleActivator
(
LogicalKeyboardKey
.
arrowLeft
,
meta:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
false
,
collapseSelection:
true
),
SingleActivator
(
LogicalKeyboardKey
.
arrowRight
,
meta:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
true
,
collapseSelection:
true
),
...
...
@@ -321,6 +325,14 @@ class DefaultTextEditingShortcuts extends Shortcuts {
SingleActivator
(
LogicalKeyboardKey
.
keyC
,
meta:
true
):
CopySelectionTextIntent
.
copy
,
SingleActivator
(
LogicalKeyboardKey
.
keyV
,
meta:
true
):
PasteTextIntent
(
SelectionChangedCause
.
keyboard
),
SingleActivator
(
LogicalKeyboardKey
.
keyA
,
meta:
true
):
SelectAllTextIntent
(
SelectionChangedCause
.
keyboard
),
// The following key combinations have no effect on text editing on this
// platform:
// * End
// * Home
// * Control + end
// * Control + home
// * Control + shift + end
// * Control + shift + home
};
// The following key combinations have no effect on text editing on this
...
...
@@ -339,7 +351,17 @@ class DefaultTextEditingShortcuts extends Shortcuts {
// * Meta + shift + arrow up
// * Meta + delete
// * Meta + backspace
static
const
Map
<
ShortcutActivator
,
Intent
>
_windowsShortcuts
=
_linuxShortcuts
;
static
const
Map
<
ShortcutActivator
,
Intent
>
_windowsShortcuts
=
<
ShortcutActivator
,
Intent
>{
...
_commonShortcuts
,
SingleActivator
(
LogicalKeyboardKey
.
home
):
ExtendSelectionToLineBreakIntent
(
forward:
false
,
collapseSelection:
true
),
SingleActivator
(
LogicalKeyboardKey
.
end
):
ExtendSelectionToLineBreakIntent
(
forward:
true
,
collapseSelection:
true
),
SingleActivator
(
LogicalKeyboardKey
.
home
,
shift:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
false
,
collapseSelection:
false
),
SingleActivator
(
LogicalKeyboardKey
.
end
,
shift:
true
):
ExtendSelectionToLineBreakIntent
(
forward:
true
,
collapseSelection:
false
),
SingleActivator
(
LogicalKeyboardKey
.
home
,
control:
true
):
ExtendSelectionToDocumentBoundaryIntent
(
forward:
false
,
collapseSelection:
true
),
SingleActivator
(
LogicalKeyboardKey
.
end
,
control:
true
):
ExtendSelectionToDocumentBoundaryIntent
(
forward:
true
,
collapseSelection:
true
),
SingleActivator
(
LogicalKeyboardKey
.
home
,
shift:
true
,
control:
true
):
ExtendSelectionToDocumentBoundaryIntent
(
forward:
false
,
collapseSelection:
false
),
SingleActivator
(
LogicalKeyboardKey
.
end
,
shift:
true
,
control:
true
):
ExtendSelectionToDocumentBoundaryIntent
(
forward:
true
,
collapseSelection:
false
),
};
// Web handles its text selection natively and doesn't use any of these
// shortcuts in Flutter.
...
...
@@ -384,6 +406,8 @@ class DefaultTextEditingShortcuts extends Shortcuts {
SingleActivator
(
LogicalKeyboardKey
.
arrowUp
,
shift:
true
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
end
,
shift:
true
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
home
,
shift:
true
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
end
,
control:
true
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
home
,
control:
true
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
space
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
keyX
,
control:
true
):
DoNothingAndStopPropagationTextIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
keyX
,
meta:
true
):
DoNothingAndStopPropagationTextIntent
(),
...
...
packages/flutter/lib/src/widgets/editable_text.dart
View file @
a1115b8b
...
...
@@ -3639,6 +3639,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
UpdateSelectionIntent
(
state
.
_value
,
_collapse
(
textBoundarySelection
),
SelectionChangedCause
.
keyboard
),
);
}
final
TextPosition
extent
=
textBoundarySelection
.
extent
;
final
TextPosition
newExtent
=
intent
.
forward
?
textBoundary
.
getTrailingTextBoundaryAt
(
extent
)
...
...
@@ -3648,6 +3649,20 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
?
TextSelection
.
fromPosition
(
newExtent
)
:
textBoundarySelection
.
extendTo
(
newExtent
);
// If collapseAtReversal is true and would have an effect, collapse it.
if
(!
selection
.
isCollapsed
&&
intent
.
collapseAtReversal
&&
(
selection
.
baseOffset
<
selection
.
extentOffset
!=
newSelection
.
baseOffset
<
newSelection
.
extentOffset
))
{
return
Actions
.
invoke
(
context
!,
UpdateSelectionIntent
(
state
.
_value
,
TextSelection
.
fromPosition
(
selection
.
base
),
SelectionChangedCause
.
keyboard
,
),
);
}
return
Actions
.
invoke
(
context
!,
UpdateSelectionIntent
(
textBoundary
.
textEditingValue
,
newSelection
,
SelectionChangedCause
.
keyboard
),
...
...
packages/flutter/lib/src/widgets/text_editing_action_target.dart
deleted
100644 → 0
View file @
f62a1280
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
show
TextAffinity
,
TextPosition
;
import
'package:characters/characters.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
show
Clipboard
,
ClipboardData
,
TextLayoutMetrics
,
TextRange
;
import
'editable_text.dart'
;
/// The recipient of a [TextEditingAction].
///
/// TextEditingActions will only be enabled when an implementer of this class is
/// focused.
///
/// See also:
///
/// * [EditableTextState], which implements this and is the most typical
/// target of a TextEditingAction.
abstract
class
TextEditingActionTarget
{
/// Whether the characters in the field are obscured from the user.
///
/// When true, the entire contents of the field are treated as one word.
bool
get
obscureText
;
/// Whether the field currently in a read-only state.
///
/// When true, [textEditingValue]'s text may not be modified, but its selection can be.
bool
get
readOnly
;
/// Whether the [textEditingValue]'s selection can be modified.
bool
get
selectionEnabled
;
/// Provides information about the text that is the target of this action.
///
/// See also:
///
/// * [EditableTextState.renderEditable], which overrides this.
TextLayoutMetrics
get
textLayoutMetrics
;
/// The [TextEditingValue] expressed in this field.
TextEditingValue
get
textEditingValue
;
// Holds the last cursor location the user selected in the case the user tries
// to select vertically past the end or beginning of the field. If they do,
// then we need to keep the old cursor location so that we can go back to it
// if they change their minds. Only used for moving selection up and down in a
// multiline text field when selecting using the keyboard.
int
_cursorResetLocation
=
-
1
;
// Whether we should reset the location of the cursor in the case the user
// tries to select vertically past the end or beginning of the field. If they
// do, then we need to keep the old cursor location so that we can go back to
// it if they change their minds. Only used for resetting selection up and
// down in a multiline text field when selecting using the keyboard.
bool
_wasSelectingVerticallyWithKeyboard
=
false
;
/// Called when assuming that the text layout is in sync with
/// [textEditingValue].
///
/// Can be overridden to assert that this is a valid assumption.
void
debugAssertLayoutUpToDate
();
/// Returns the index into the string of the next character boundary after the
/// given index.
///
/// The character boundary is determined by the characters package, so
/// surrogate pairs and extended grapheme clusters are considered.
///
/// The index must be between 0 and string.length, inclusive. If given
/// string.length, string.length is returned.
///
/// Setting includeWhitespace to false will only return the index of non-space
/// characters.
@visibleForTesting
static
int
nextCharacter
(
int
index
,
String
string
,
[
bool
includeWhitespace
=
true
])
{
assert
(
index
>=
0
&&
index
<=
string
.
length
);
if
(
index
==
string
.
length
)
{
return
string
.
length
;
}
final
CharacterRange
range
=
CharacterRange
.
at
(
string
,
0
,
index
);
// If index is not on a character boundary, return the next character
// boundary.
if
(
range
.
current
.
length
!=
index
)
{
return
range
.
current
.
length
;
}
range
.
expandNext
();
if
(!
includeWhitespace
)
{
range
.
expandWhile
((
String
character
)
{
return
TextLayoutMetrics
.
isWhitespace
(
character
.
codeUnitAt
(
0
));
});
}
return
range
.
current
.
length
;
}
/// Returns the index into the string of the previous character boundary
/// before the given index.
///
/// The character boundary is determined by the characters package, so
/// surrogate pairs and extended grapheme clusters are considered.
///
/// The index must be between 0 and string.length, inclusive. If index is 0,
/// 0 will be returned.
///
/// Setting includeWhitespace to false will only return the index of non-space
/// characters.
@visibleForTesting
static
int
previousCharacter
(
int
index
,
String
string
,
[
bool
includeWhitespace
=
true
])
{
assert
(
index
>=
0
&&
index
<=
string
.
length
);
if
(
index
==
0
)
{
return
0
;
}
final
CharacterRange
range
=
CharacterRange
.
at
(
string
,
0
,
index
);
// If index is not on a character boundary, return the previous character
// boundary.
if
(
range
.
current
.
length
!=
index
)
{
range
.
dropLast
();
return
range
.
current
.
length
;
}
range
.
dropLast
();
if
(!
includeWhitespace
)
{
while
(
range
.
currentCharacters
.
isNotEmpty
&&
TextLayoutMetrics
.
isWhitespace
(
range
.
charactersAfter
.
first
.
codeUnitAt
(
0
)))
{
range
.
dropLast
();
}
}
return
range
.
current
.
length
;
}
/// {@template flutter.widgets.TextEditingActionTarget.setSelection}
/// Called to update the [TextSelection] in the current [TextEditingValue].
/// {@endtemplate}
void
setSelection
(
TextSelection
nextSelection
,
SelectionChangedCause
cause
)
{
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
setTextEditingValue
(
textEditingValue
.
copyWith
(
selection:
nextSelection
),
cause
,
);
}
/// {@template flutter.widgets.TextEditingActionTarget.setTextEditingValue}
/// Called to update the current [TextEditingValue].
/// {@endtemplate}
void
setTextEditingValue
(
TextEditingValue
newValue
,
SelectionChangedCause
cause
);
// Extend the current selection to the end of the field.
//
// If selectionEnabled is false, keeps the selection collapsed and moves it to
// the end.
//
// See also:
//
// * _extendSelectionToStart
void
_extendSelectionToEnd
(
SelectionChangedCause
cause
)
{
if
(
textEditingValue
.
selection
.
extentOffset
==
textEditingValue
.
text
.
length
)
{
return
;
}
final
TextSelection
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
extentOffset:
textEditingValue
.
text
.
length
,
);
return
setSelection
(
nextSelection
,
cause
);
}
// Extend the current selection to the start of the field.
//
// If selectionEnabled is false, keeps the selection collapsed and moves it to
// the start.
//
// The given [SelectionChangedCause] indicates the cause of this change and
// will be passed to [setSelection].
//
// See also:
//
// * _extendSelectionToEnd
void
_extendSelectionToStart
(
SelectionChangedCause
cause
)
{
if
(!
selectionEnabled
)
{
return
moveSelectionToStart
(
cause
);
}
setSelection
(
textEditingValue
.
selection
.
extendTo
(
const
TextPosition
(
offset:
0
,
affinity:
TextAffinity
.
upstream
,
)),
cause
);
}
// Return the offset at the start of the nearest word to the left of the
// given offset.
int
_getLeftByWord
(
int
offset
,
[
bool
includeWhitespace
=
true
])
{
// If the offset is already all the way left, there is nothing to do.
if
(
offset
<=
0
)
{
return
offset
;
}
// If we can just return the start of the text without checking for a word.
if
(
offset
==
1
)
{
return
0
;
}
final
int
startPoint
=
previousCharacter
(
offset
,
textEditingValue
.
text
,
includeWhitespace
);
final
TextRange
word
=
textLayoutMetrics
.
getWordBoundary
(
TextPosition
(
offset:
startPoint
,
affinity:
textEditingValue
.
selection
.
affinity
));
return
word
.
start
;
}
/// Return the offset at the end of the nearest word to the right of the given
/// offset.
int
_getRightByWord
(
int
offset
,
[
bool
includeWhitespace
=
true
])
{
// If the selection is already all the way right, there is nothing to do.
if
(
offset
==
textEditingValue
.
text
.
length
)
{
return
offset
;
}
// If we can just return the end of the text without checking for a word.
if
(
offset
==
textEditingValue
.
text
.
length
-
1
||
offset
==
textEditingValue
.
text
.
length
)
{
return
textEditingValue
.
text
.
length
;
}
final
int
startPoint
=
includeWhitespace
||
!
TextLayoutMetrics
.
isWhitespace
(
textEditingValue
.
text
.
codeUnitAt
(
offset
))
?
offset
:
nextCharacter
(
offset
,
textEditingValue
.
text
,
includeWhitespace
);
final
TextRange
nextWord
=
textLayoutMetrics
.
getWordBoundary
(
TextPosition
(
offset:
startPoint
,
affinity:
textEditingValue
.
selection
.
affinity
));
return
nextWord
.
end
;
}
// Deletes the current non-empty selection.
//
// If the selection is currently non-empty, this method deletes the selected
// text. Otherwise this method does nothing.
TextEditingValue
_deleteNonEmptySelection
()
{
assert
(
textEditingValue
.
selection
.
isValid
);
assert
(!
textEditingValue
.
selection
.
isCollapsed
);
final
String
textBefore
=
textEditingValue
.
selection
.
textBefore
(
textEditingValue
.
text
);
final
String
textAfter
=
textEditingValue
.
selection
.
textAfter
(
textEditingValue
.
text
);
final
TextSelection
newSelection
=
TextSelection
.
collapsed
(
offset:
textEditingValue
.
selection
.
start
,
affinity:
textEditingValue
.
selection
.
affinity
,
);
final
TextRange
newComposingRange
=
!
textEditingValue
.
composing
.
isValid
||
textEditingValue
.
composing
.
isCollapsed
?
TextRange
.
empty
:
TextRange
(
start:
textEditingValue
.
composing
.
start
-
(
textEditingValue
.
composing
.
start
-
textEditingValue
.
selection
.
start
).
clamp
(
0
,
textEditingValue
.
selection
.
end
-
textEditingValue
.
selection
.
start
),
end:
textEditingValue
.
composing
.
end
-
(
textEditingValue
.
composing
.
end
-
textEditingValue
.
selection
.
start
).
clamp
(
0
,
textEditingValue
.
selection
.
end
-
textEditingValue
.
selection
.
start
),
);
return
TextEditingValue
(
text:
textBefore
+
textAfter
,
selection:
newSelection
,
composing:
newComposingRange
,
);
}
/// Returns a new TextEditingValue representing a deletion from the current
/// [selection] to the given index, inclusively.
///
/// If the selection is not collapsed, deletes the selection regardless of the
/// given index.
///
/// The composing region, if any, will also be adjusted to remove the deleted
/// characters.
TextEditingValue
_deleteTo
(
TextPosition
position
)
{
assert
(
textEditingValue
.
selection
!=
null
);
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
textEditingValue
;
}
if
(!
textEditingValue
.
selection
.
isCollapsed
)
{
return
_deleteNonEmptySelection
();
}
if
(
position
.
offset
==
textEditingValue
.
selection
.
extentOffset
)
{
return
textEditingValue
;
}
final
TextRange
deletion
=
TextRange
(
start:
math
.
min
(
position
.
offset
,
textEditingValue
.
selection
.
extentOffset
),
end:
math
.
max
(
position
.
offset
,
textEditingValue
.
selection
.
extentOffset
),
);
final
String
deleted
=
deletion
.
textInside
(
textEditingValue
.
text
);
if
(
deletion
.
textInside
(
textEditingValue
.
text
).
isEmpty
)
{
return
textEditingValue
;
}
final
int
charactersDeletedBeforeComposingStart
=
(
textEditingValue
.
composing
.
start
-
deletion
.
start
).
clamp
(
0
,
deleted
.
length
);
final
int
charactersDeletedBeforeComposingEnd
=
(
textEditingValue
.
composing
.
end
-
deletion
.
start
).
clamp
(
0
,
deleted
.
length
);
final
TextRange
nextComposingRange
=
!
textEditingValue
.
composing
.
isValid
||
textEditingValue
.
composing
.
isCollapsed
?
TextRange
.
empty
:
TextRange
(
start:
textEditingValue
.
composing
.
start
-
charactersDeletedBeforeComposingStart
,
end:
textEditingValue
.
composing
.
end
-
charactersDeletedBeforeComposingEnd
,
);
return
TextEditingValue
(
text:
deletion
.
textBefore
(
textEditingValue
.
text
)
+
deletion
.
textAfter
(
textEditingValue
.
text
),
selection:
TextSelection
.
collapsed
(
offset:
deletion
.
start
,
affinity:
position
.
affinity
,
),
composing:
nextComposingRange
,
);
}
/// Deletes backwards from the current selection.
///
/// If the selection is collapsed, deletes a single character before the
/// cursor.
///
/// If the selection is not collapsed, deletes the selection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@template flutter.widgets.TextEditingActionTarget.cause}
/// The given [SelectionChangedCause] indicates the cause of this change and
/// will be passed to [setSelection].
/// {@endtemplate}
///
/// See also:
///
/// * [deleteForward], which is same but in the opposite direction.
void
delete
(
SelectionChangedCause
cause
)
{
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// `delete` does not depend on the text layout, and the boundary analysis is
// done using the `previousCharacter` method instead of ICU, we can keep
// deleting without having to layout the text. For this reason, we can
// directly delete the character before the caret in the controller.
final
String
textBefore
=
textEditingValue
.
selection
.
textBefore
(
textEditingValue
.
text
);
final
int
characterBoundary
=
previousCharacter
(
textBefore
.
length
,
textBefore
,
);
final
TextPosition
position
=
TextPosition
(
offset:
characterBoundary
);
setTextEditingValue
(
_deleteTo
(
position
),
cause
);
}
/// Deletes a word backwards from the current selection.
///
/// If the selection is collapsed, deletes a word before the cursor.
///
/// If the selection is not collapsed, deletes the selection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// If [obscureText] is true, it treats the whole text content as a single
/// word.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// {@template flutter.widgets.TextEditingActionTarget.whiteSpace}
/// By default, includeWhitespace is set to true, meaning that whitespace can
/// be considered a word in itself. If set to false, the selection will be
/// extended past any whitespace and the first word following the whitespace.
/// {@endtemplate}
///
/// See also:
///
/// * [deleteForwardByWord], which is same but in the opposite direction.
void
deleteByWord
(
SelectionChangedCause
cause
,
[
bool
includeWhitespace
=
true
])
{
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(
obscureText
)
{
// When the text is obscured, the whole thing is treated as one big line.
return
deleteToStart
(
cause
);
}
final
String
textBefore
=
textEditingValue
.
selection
.
textBefore
(
textEditingValue
.
text
);
final
int
characterBoundary
=
_getLeftByWord
(
textBefore
.
length
,
includeWhitespace
);
final
TextEditingValue
nextValue
=
_deleteTo
(
TextPosition
(
offset:
characterBoundary
));
setTextEditingValue
(
nextValue
,
cause
);
}
/// Deletes a line backwards from the current selection.
///
/// If the selection is collapsed, deletes a line before the cursor.
///
/// If the selection is not collapsed, deletes the selection.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [deleteForwardByLine], which is same but in the opposite direction.
void
deleteByLine
(
SelectionChangedCause
cause
)
{
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// When there is a line break, line delete shouldn't do anything
final
String
textBefore
=
textEditingValue
.
selection
.
textBefore
(
textEditingValue
.
text
);
final
bool
isPreviousCharacterBreakLine
=
textBefore
.
codeUnitAt
(
textBefore
.
length
-
1
)
==
0x0A
;
if
(
isPreviousCharacterBreakLine
)
{
return
;
}
// When the text is obscured, the whole thing is treated as one big line.
if
(
obscureText
)
{
return
deleteToStart
(
cause
);
}
final
TextSelection
line
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
textBefore
.
length
-
1
),
);
setTextEditingValue
(
_deleteTo
(
TextPosition
(
offset:
line
.
start
)),
cause
);
}
/// Deletes in the forward direction.
///
/// If the selection is collapsed, deletes a single character after the
/// cursor.
///
/// If the selection is not collapsed, deletes the selection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [delete], which is the same but in the opposite direction.
void
deleteForward
(
SelectionChangedCause
cause
)
{
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
final
String
textAfter
=
textEditingValue
.
selection
.
textAfter
(
textEditingValue
.
text
);
final
int
characterBoundary
=
nextCharacter
(
0
,
textAfter
);
setTextEditingValue
(
_deleteTo
(
TextPosition
(
offset:
textEditingValue
.
selection
.
end
+
characterBoundary
)),
cause
);
}
/// Deletes a word in the forward direction from the current selection.
///
/// If the selection is collapsed, deletes a word after the cursor.
///
/// If the selection is not collapsed, deletes the selection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
///
/// See also:
///
/// * [deleteByWord], which is same but in the opposite direction.
void
deleteForwardByWord
(
SelectionChangedCause
cause
,
[
bool
includeWhitespace
=
true
])
{
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(
obscureText
)
{
// When the text is obscured, the whole thing is treated as one big word.
return
deleteToEnd
(
cause
);
}
final
String
textBefore
=
textEditingValue
.
selection
.
textBefore
(
textEditingValue
.
text
);
final
int
characterBoundary
=
_getRightByWord
(
textBefore
.
length
,
includeWhitespace
);
final
TextEditingValue
nextValue
=
_deleteTo
(
TextPosition
(
offset:
characterBoundary
));
setTextEditingValue
(
nextValue
,
cause
);
}
/// Deletes a line in the forward direction from the current selection.
///
/// If the selection is collapsed, deletes a line after the cursor.
///
/// If the selection is not collapsed, deletes the selection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// If [obscureText] is true, it treats the whole text content as
/// a single word.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [deleteByLine], which is same but in the opposite direction.
void
deleteForwardByLine
(
SelectionChangedCause
cause
)
{
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(
obscureText
)
{
// When the text is obscured, the whole thing is treated as one big line.
return
deleteToEnd
(
cause
);
}
// When there is a line break, it shouldn't do anything.
final
String
textAfter
=
textEditingValue
.
selection
.
textAfter
(
textEditingValue
.
text
);
final
bool
isNextCharacterBreakLine
=
textAfter
.
codeUnitAt
(
0
)
==
0x0A
;
if
(
isNextCharacterBreakLine
)
{
return
;
}
final
String
textBefore
=
textEditingValue
.
selection
.
textBefore
(
textEditingValue
.
text
);
final
TextSelection
line
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
textBefore
.
length
),
);
setTextEditingValue
(
_deleteTo
(
TextPosition
(
offset:
line
.
end
)),
cause
);
}
/// Deletes the from the current collapsed selection to the end of the field.
///
/// The given SelectionChangedCause indicates the cause of this change and
/// will be passed to setSelection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// See also:
/// * [deleteToStart]
void
deleteToEnd
(
SelectionChangedCause
cause
)
{
assert
(
textEditingValue
.
selection
.
isCollapsed
);
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
setTextEditingValue
(
_deleteTo
(
TextPosition
(
offset:
textEditingValue
.
text
.
length
)),
cause
);
}
/// Deletes the from the current collapsed selection to the start of the field.
///
/// The given SelectionChangedCause indicates the cause of this change and
/// will be passed to setSelection.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// See also:
/// * [deleteToEnd]
void
deleteToStart
(
SelectionChangedCause
cause
)
{
assert
(
textEditingValue
.
selection
.
isCollapsed
);
if
(
readOnly
)
{
return
;
}
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
setTextEditingValue
(
_deleteTo
(
const
TextPosition
(
offset:
0
)),
cause
);
}
/// Expand the current selection to the end of the field.
///
/// The selection will never shrink. The [TextSelection.extentOffset] will
// always be at the end of the field, regardless of the original order of
/// [TextSelection.baseOffset] and [TextSelection.extentOffset].
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// to the end.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [expandSelectionToStart], which is same but in the opposite direction.
void
expandSelectionToEnd
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionToEnd
(
cause
);
}
final
TextPosition
nextPosition
=
TextPosition
(
offset:
textEditingValue
.
text
.
length
,
);
setSelection
(
textEditingValue
.
selection
.
expandTo
(
nextPosition
,
true
),
cause
);
}
/// Expand the current selection to the start of the field.
///
/// The selection will never shrink. The [TextSelection.extentOffset] will
/// always be at the start of the field, regardless of the original order of
/// [TextSelection.baseOffset] and [TextSelection.extentOffset].
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// to the start.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [expandSelectionToEnd], which is the same but in the opposite
/// direction.
void
expandSelectionToStart
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionToStart
(
cause
);
}
const
TextPosition
nextPosition
=
TextPosition
(
offset:
0
,
affinity:
TextAffinity
.
upstream
,
);
setSelection
(
textEditingValue
.
selection
.
expandTo
(
nextPosition
,
true
),
cause
);
}
/// Expand the current selection to the smallest selection that includes the
/// start of the line.
///
/// The selection will never shrink. The upper offset will be expanded to the
/// beginning of its line, and the original order of baseOffset and
/// [TextSelection.extentOffset] will be preserved.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left by line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [expandSelectionRightByLine], which is the same but in the opposite
/// direction.
void
expandSelectionLeftByLine
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionLeftByLine
(
cause
);
}
// If the lowest edge of the selection is at the start of a line, don't do
// anything.
// TODO(justinmc): Support selection with multiple TextAffinities.
// https://github.com/flutter/flutter/issues/88135
final
TextSelection
currentLine
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
textEditingValue
.
selection
.
start
,
affinity:
textEditingValue
.
selection
.
isCollapsed
?
textEditingValue
.
selection
.
affinity
:
TextAffinity
.
downstream
,
),
);
if
(
currentLine
.
baseOffset
==
textEditingValue
.
selection
.
start
)
{
return
;
}
setSelection
(
textEditingValue
.
selection
.
expandTo
(
TextPosition
(
offset:
currentLine
.
baseOffset
,
affinity:
textEditingValue
.
selection
.
affinity
,
)),
cause
);
}
/// Expand the current selection to the smallest selection that includes the
/// end of the line.
///
/// The selection will never shrink. The lower offset will be expanded to the
/// end of its line and the original order of [TextSelection.baseOffset] and
/// [TextSelection.extentOffset] will be preserved.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right by line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [expandSelectionLeftByLine], which is the same but in the opposite
/// direction.
void
expandSelectionRightByLine
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionRightByLine
(
cause
);
}
// If greatest edge is already at the end of a line, don't do anything.
// TODO(justinmc): Support selection with multiple TextAffinities.
// https://github.com/flutter/flutter/issues/88135
final
TextSelection
currentLine
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
textEditingValue
.
selection
.
end
,
affinity:
textEditingValue
.
selection
.
isCollapsed
?
textEditingValue
.
selection
.
affinity
:
TextAffinity
.
upstream
,
),
);
if
(
currentLine
.
extentOffset
==
textEditingValue
.
selection
.
end
)
{
return
;
}
final
TextSelection
nextSelection
=
textEditingValue
.
selection
.
expandTo
(
TextPosition
(
offset:
currentLine
.
extentOffset
,
affinity:
TextAffinity
.
upstream
,
),
);
setSelection
(
nextSelection
,
cause
);
}
/// Keeping selection's [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] down by one line.
///
/// If selectionEnabled is false, keeps the selection collapsed and just
/// moves it down.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [extendSelectionUp], which is same but in the opposite direction.
void
extendSelectionDown
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionDown
(
cause
);
}
// If the selection is collapsed at the end of the field already, then
// nothing happens.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
>=
textEditingValue
.
text
.
length
)
{
return
;
}
int
index
=
textLayoutMetrics
.
getTextPositionBelow
(
textEditingValue
.
selection
.
extent
).
offset
;
if
(
index
==
textEditingValue
.
selection
.
extentOffset
)
{
index
=
textEditingValue
.
text
.
length
;
_wasSelectingVerticallyWithKeyboard
=
true
;
}
else
if
(
_wasSelectingVerticallyWithKeyboard
)
{
index
=
_cursorResetLocation
;
_wasSelectingVerticallyWithKeyboard
=
false
;
}
else
{
_cursorResetLocation
=
index
;
}
final
TextPosition
nextPosition
=
TextPosition
(
offset:
index
,
affinity:
textEditingValue
.
selection
.
affinity
,
);
setSelection
(
textEditingValue
.
selection
.
extendTo
(
nextPosition
),
cause
);
}
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [extendSelectionRight], which is same but in the opposite direction.
void
extendSelectionLeft
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionLeft
(
cause
);
}
// If the selection is already all the way left, there is nothing to do.
if
(
textEditingValue
.
selection
.
extentOffset
<=
0
)
{
return
;
}
final
int
previousExtent
=
previousCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
,
);
final
int
distance
=
textEditingValue
.
selection
.
extentOffset
-
previousExtent
;
_cursorResetLocation
-=
distance
;
setSelection
(
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
previousExtent
,
affinity:
textEditingValue
.
selection
.
affinity
)),
cause
);
}
/// Extend the current selection to the start of
/// [TextSelection.extentOffset]'s line.
///
/// Uses [TextSelection.baseOffset] as a pivot point and doesn't change it.
/// If [TextSelection.extentOffset] is right of [TextSelection.baseOffset],
/// then the selection will be collapsed.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// left by line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [extendSelectionRightByLine], which is same but in the opposite
/// direction.
/// * [expandSelectionRightByLine], which strictly grows the selection
/// regardless of the order.
void
extendSelectionLeftByLine
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionLeftByLine
(
cause
);
}
// When going left, we want to skip over any whitespace before the line,
// so we go back to the first non-whitespace before asking for the line
// bounds, since getLineAtOffset finds the line boundaries without
// including whitespace (like the newline).
final
int
startPoint
=
previousCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
,
false
);
final
TextSelection
selectedLine
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
startPoint
),
);
late
final
TextSelection
nextSelection
;
// If the extent and base offsets would reverse order, then instead the
// selection collapses.
if
(
textEditingValue
.
selection
.
extentOffset
>
textEditingValue
.
selection
.
baseOffset
)
{
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
extentOffset:
textEditingValue
.
selection
.
baseOffset
,
);
}
else
{
nextSelection
=
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
selectedLine
.
baseOffset
,
));
}
setSelection
(
nextSelection
,
cause
);
}
/// Keeping selection's [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] right.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [extendSelectionLeft], which is same but in the opposite direction.
void
extendSelectionRight
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionRight
(
cause
);
}
// If the selection is already all the way right, there is nothing to do.
if
(
textEditingValue
.
selection
.
extentOffset
>=
textEditingValue
.
text
.
length
)
{
return
;
}
final
int
nextExtent
=
nextCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
);
final
int
distance
=
nextExtent
-
textEditingValue
.
selection
.
extentOffset
;
_cursorResetLocation
+=
distance
;
setSelection
(
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
nextExtent
,
affinity:
textEditingValue
.
selection
.
affinity
)),
cause
);
}
/// Extend the current selection to the end of [TextSelection.extentOffset]'s
/// line.
///
/// Uses [TextSelection.baseOffset] as a pivot point and doesn't change it. If
/// [TextSelection.extentOffset] is left of [TextSelection.baseOffset], then
/// collapses the selection.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// right by line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [extendSelectionLeftByLine], which is same but in the opposite
/// direction.
/// * [expandSelectionRightByLine], which strictly grows the selection
/// regardless of the order.
void
extendSelectionRightByLine
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionRightByLine
(
cause
);
}
final
int
startPoint
=
nextCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
,
false
);
final
TextSelection
selectedLine
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
startPoint
),
);
// If the extent and base offsets would reverse order, then instead the
// selection collapses.
late
final
TextSelection
nextSelection
;
if
(
textEditingValue
.
selection
.
extentOffset
<
textEditingValue
.
selection
.
baseOffset
)
{
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
extentOffset:
textEditingValue
.
selection
.
baseOffset
,
);
}
else
{
nextSelection
=
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
selectedLine
.
extentOffset
,
affinity:
TextAffinity
.
upstream
,
));
}
setSelection
(
nextSelection
,
cause
);
}
/// Extend the current selection to the previous start of a word.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
///
/// {@template flutter.widgets.TextEditingActionTarget.stopAtReversal}
/// The `stopAtReversal` parameter is false by default, meaning that it's
/// ok for the base and extent to flip their order here. If set to true, then
/// the selection will collapse when it would otherwise reverse its order. A
/// selection that is already collapsed is not affected by this parameter.
/// {@endtemplate}
///
/// See also:
///
/// * [extendSelectionRightByWord], which is the same but in the opposite
/// direction.
void
extendSelectionLeftByWord
(
SelectionChangedCause
cause
,
[
bool
includeWhitespace
=
true
,
bool
stopAtReversal
=
false
])
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// When the text is obscured, the whole thing is treated as one big word.
if
(
obscureText
)
{
return
_extendSelectionToStart
(
cause
);
}
debugAssertLayoutUpToDate
();
// If the selection is already all the way left, there is nothing to do.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
<=
0
)
{
return
;
}
final
int
leftOffset
=
_getLeftByWord
(
textEditingValue
.
selection
.
extentOffset
,
includeWhitespace
);
late
final
TextSelection
nextSelection
;
if
(
stopAtReversal
&&
textEditingValue
.
selection
.
extentOffset
>
textEditingValue
.
selection
.
baseOffset
&&
leftOffset
<
textEditingValue
.
selection
.
baseOffset
)
{
nextSelection
=
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
textEditingValue
.
selection
.
baseOffset
));
}
else
{
nextSelection
=
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
leftOffset
,
affinity:
textEditingValue
.
selection
.
affinity
));
}
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Extend the current selection to the next end of a word.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
///
/// {@macro flutter.widgets.TextEditingActionTarget.stopAtReversal}
///
/// See also:
///
/// * [extendSelectionLeftByWord], which is the same but in the opposite
/// direction.
void
extendSelectionRightByWord
(
SelectionChangedCause
cause
,
[
bool
includeWhitespace
=
true
,
bool
stopAtReversal
=
false
])
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
debugAssertLayoutUpToDate
();
// When the text is obscured, the whole thing is treated as one big word.
if
(
obscureText
)
{
return
_extendSelectionToEnd
(
cause
);
}
// If the selection is already all the way right, there is nothing to do.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
==
textEditingValue
.
text
.
length
)
{
return
;
}
final
int
rightOffset
=
_getRightByWord
(
textEditingValue
.
selection
.
extentOffset
,
includeWhitespace
);
late
final
TextSelection
nextSelection
;
if
(
stopAtReversal
&&
textEditingValue
.
selection
.
baseOffset
>
textEditingValue
.
selection
.
extentOffset
&&
rightOffset
>
textEditingValue
.
selection
.
baseOffset
)
{
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
textEditingValue
.
selection
.
baseOffset
),
);
}
else
{
nextSelection
=
textEditingValue
.
selection
.
extendTo
(
TextPosition
(
offset:
rightOffset
,
affinity:
textEditingValue
.
selection
.
affinity
));
}
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Keeping selection's [TextSelection.baseOffset] fixed, move the
/// [TextSelection.extentOffset] up by one
/// line.
///
/// If [selectionEnabled] is false, keeps the selection collapsed and moves it
/// up.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [extendSelectionDown], which is the same but in the opposite
/// direction.
void
extendSelectionUp
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
if
(!
selectionEnabled
)
{
return
moveSelectionUp
(
cause
);
}
// If the selection is collapsed at the beginning of the field already, then
// nothing happens.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
<=
0.0
)
{
return
;
}
final
TextPosition
positionAbove
=
textLayoutMetrics
.
getTextPositionAbove
(
textEditingValue
.
selection
.
extent
);
late
final
TextSelection
nextSelection
;
if
(
positionAbove
.
offset
==
textEditingValue
.
selection
.
extentOffset
)
{
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
extentOffset:
0
,
affinity:
TextAffinity
.
upstream
,
);
_wasSelectingVerticallyWithKeyboard
=
true
;
}
else
if
(
_wasSelectingVerticallyWithKeyboard
)
{
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
baseOffset:
textEditingValue
.
selection
.
baseOffset
,
extentOffset:
_cursorResetLocation
,
);
_wasSelectingVerticallyWithKeyboard
=
false
;
}
else
{
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
baseOffset:
textEditingValue
.
selection
.
baseOffset
,
extentOffset:
positionAbove
.
offset
,
affinity:
positionAbove
.
affinity
,
);
_cursorResetLocation
=
nextSelection
.
extentOffset
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the leftmost point of the current line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionRightByLine], which is the same but in the opposite
/// direction.
void
moveSelectionLeftByLine
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// If already at the left edge of the line, do nothing.
final
TextSelection
currentLine
=
textLayoutMetrics
.
getLineAtOffset
(
textEditingValue
.
selection
.
extent
,
);
if
(
currentLine
.
baseOffset
==
textEditingValue
.
selection
.
extentOffset
)
{
return
;
}
// When going left, we want to skip over any whitespace before the line,
// so we go back to the first non-whitespace before asking for the line
// bounds, since getLineAtOffset finds the line boundaries without
// including whitespace (like the newline).
final
int
startPoint
=
previousCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
,
false
);
final
TextSelection
selectedLine
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
startPoint
),
);
final
TextSelection
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
selectedLine
.
baseOffset
,
));
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the next line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionUp], which is the same but in the opposite direction.
void
moveSelectionDown
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// If the selection is collapsed at the end of the field already, then
// nothing happens.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
>=
textEditingValue
.
text
.
length
)
{
return
;
}
final
TextPosition
positionBelow
=
textLayoutMetrics
.
getTextPositionBelow
(
textEditingValue
.
selection
.
extent
);
late
final
TextSelection
nextSelection
;
if
(
positionBelow
.
offset
==
textEditingValue
.
selection
.
extentOffset
)
{
nextSelection
=
textEditingValue
.
selection
.
copyWith
(
baseOffset:
textEditingValue
.
text
.
length
,
extentOffset:
textEditingValue
.
text
.
length
,
);
}
else
{
nextSelection
=
TextSelection
.
fromPosition
(
positionBelow
);
}
if
(
textEditingValue
.
selection
.
extentOffset
==
textEditingValue
.
text
.
length
)
{
_wasSelectingVerticallyWithKeyboard
=
false
;
}
else
{
_cursorResetLocation
=
nextSelection
.
extentOffset
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection left by one character.
///
/// If it can't be moved left, do nothing.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionRight], which is the same but in the opposite direction.
void
moveSelectionLeft
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// If the selection is already all the way left, there is nothing to do.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
<=
0
)
{
return
;
}
int
previousExtent
;
if
(
textEditingValue
.
selection
.
start
!=
textEditingValue
.
selection
.
end
)
{
previousExtent
=
textEditingValue
.
selection
.
start
;
}
else
{
previousExtent
=
previousCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
);
}
final
TextSelection
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
previousExtent
,
affinity:
textEditingValue
.
selection
.
affinity
,
),
);
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
_cursorResetLocation
-=
textEditingValue
.
selection
.
extentOffset
-
nextSelection
.
extentOffset
;
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the previous start of a word.
///
/// A TextSelection that isn't collapsed will be collapsed and moved from the
/// extentOffset.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
///
/// See also:
///
/// * [moveSelectionRightByWord], which is the same but in the opposite
/// direction.
void
moveSelectionLeftByWord
(
SelectionChangedCause
cause
,
[
bool
includeWhitespace
=
true
])
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// When the text is obscured, the whole thing is treated as one big word.
if
(
obscureText
)
{
return
moveSelectionToStart
(
cause
);
}
debugAssertLayoutUpToDate
();
// If the selection is already all the way left, there is nothing to do.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
<=
0
)
{
return
;
}
final
int
leftOffset
=
_getLeftByWord
(
textEditingValue
.
selection
.
extentOffset
,
includeWhitespace
);
final
TextSelection
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
leftOffset
,
affinity:
textEditingValue
.
selection
.
affinity
));
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the right by one character.
///
/// If the selection is invalid or it can't be moved right, do nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionLeft], which is the same but in the opposite direction.
void
moveSelectionRight
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// If the selection is already all the way right, there is nothing to do.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
>=
textEditingValue
.
text
.
length
)
{
return
;
}
int
nextExtent
;
if
(
textEditingValue
.
selection
.
start
!=
textEditingValue
.
selection
.
end
)
{
nextExtent
=
textEditingValue
.
selection
.
end
;
}
else
{
nextExtent
=
nextCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
);
}
final
TextSelection
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
nextExtent
,
));
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the rightmost point of the current line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionLeftByLine], which is the same but in the opposite
/// direction.
void
moveSelectionRightByLine
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// If already at the right edge of the line, do nothing.
final
TextSelection
currentLine
=
textLayoutMetrics
.
getLineAtOffset
(
textEditingValue
.
selection
.
extent
,
);
if
(
currentLine
.
extentOffset
==
textEditingValue
.
selection
.
extentOffset
)
{
return
;
}
// When going right, we want to skip over any whitespace after the line,
// so we go forward to the first non-whitespace character before asking
// for the line bounds, since getLineAtOffset finds the line
// boundaries without including whitespace (like the newline).
final
int
startPoint
=
nextCharacter
(
textEditingValue
.
selection
.
extentOffset
,
textEditingValue
.
text
,
false
);
final
TextSelection
selectedLine
=
textLayoutMetrics
.
getLineAtOffset
(
TextPosition
(
offset:
startPoint
,
affinity:
TextAffinity
.
upstream
,
),
);
final
TextSelection
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
selectedLine
.
extentOffset
,
affinity:
TextAffinity
.
upstream
,
));
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the next end of a word.
///
/// A TextSelection that isn't collapsed will be collapsed and moved from the
/// extentOffset.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// {@macro flutter.widgets.TextEditingActionTarget.whiteSpace}
///
/// See also:
///
/// * [moveSelectionLeftByWord], which is the same but in the opposite
/// direction.
void
moveSelectionRightByWord
(
SelectionChangedCause
cause
,
[
bool
includeWhitespace
=
true
])
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
// When the text is obscured, the whole thing is treated as one big word.
if
(
obscureText
)
{
return
moveSelectionToEnd
(
cause
);
}
debugAssertLayoutUpToDate
();
// If the selection is already all the way right, there is nothing to do.
if
(
textEditingValue
.
selection
.
isCollapsed
&&
textEditingValue
.
selection
.
extentOffset
==
textEditingValue
.
text
.
length
)
{
return
;
}
final
int
rightOffset
=
_getRightByWord
(
textEditingValue
.
selection
.
extentOffset
,
includeWhitespace
);
final
TextSelection
nextSelection
=
TextSelection
.
fromPosition
(
TextPosition
(
offset:
rightOffset
,
affinity:
textEditingValue
.
selection
.
affinity
));
if
(
nextSelection
==
textEditingValue
.
selection
)
{
return
;
}
setSelection
(
nextSelection
,
cause
);
}
/// Move the current selection to the end of the field.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionToStart], which is the same but in the opposite
/// direction.
void
moveSelectionToEnd
(
SelectionChangedCause
cause
)
{
final
TextPosition
nextPosition
=
TextPosition
(
offset:
textEditingValue
.
text
.
length
,
);
setSelection
(
TextSelection
.
fromPosition
(
nextPosition
),
cause
);
}
/// Move the current selection to the start of the field.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionToEnd], which is the same but in the opposite direction.
void
moveSelectionToStart
(
SelectionChangedCause
cause
)
{
const
TextPosition
nextPosition
=
TextPosition
(
offset:
0
,
affinity:
TextAffinity
.
upstream
,
);
setSelection
(
TextSelection
.
fromPosition
(
nextPosition
),
cause
);
}
/// Move the current selection up by one line.
///
/// If the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
///
/// See also:
///
/// * [moveSelectionDown], which is the same but in the opposite direction.
void
moveSelectionUp
(
SelectionChangedCause
cause
)
{
if
(!
textEditingValue
.
selection
.
isValid
)
{
return
;
}
final
int
nextIndex
=
textLayoutMetrics
.
getTextPositionAbove
(
textEditingValue
.
selection
.
extent
).
offset
;
if
(
nextIndex
==
textEditingValue
.
selection
.
extentOffset
)
{
_wasSelectingVerticallyWithKeyboard
=
false
;
return
moveSelectionToStart
(
cause
);
}
_cursorResetLocation
=
nextIndex
;
setSelection
(
TextSelection
.
fromPosition
(
TextPosition
(
offset:
nextIndex
,
affinity:
textEditingValue
.
selection
.
affinity
)),
cause
);
}
/// Select the entire text value.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
void
selectAll
(
SelectionChangedCause
cause
)
{
setSelection
(
textEditingValue
.
selection
.
copyWith
(
baseOffset:
0
,
extentOffset:
textEditingValue
.
text
.
length
,
),
cause
,
);
}
/// {@template flutter.widgets.TextEditingActionTarget.copySelection}
/// Copy current selection to [Clipboard].
/// {@endtemplate}
///
/// If the selection is collapsed or invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
void
copySelection
(
SelectionChangedCause
cause
)
{
final
TextSelection
selection
=
textEditingValue
.
selection
;
final
String
text
=
textEditingValue
.
text
;
assert
(
selection
!=
null
);
if
(
selection
.
isCollapsed
||
!
selection
.
isValid
)
{
return
;
}
Clipboard
.
setData
(
ClipboardData
(
text:
selection
.
textInside
(
text
)));
}
/// {@template flutter.widgets.TextEditingActionTarget.cutSelection}
/// Cut current selection to Clipboard.
/// {@endtemplate}
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
void
cutSelection
(
SelectionChangedCause
cause
)
{
final
TextSelection
selection
=
textEditingValue
.
selection
;
if
(
readOnly
||
!
selection
.
isValid
)
{
return
;
}
final
String
text
=
textEditingValue
.
text
;
assert
(
selection
!=
null
);
if
(
selection
.
isCollapsed
)
{
return
;
}
Clipboard
.
setData
(
ClipboardData
(
text:
selection
.
textInside
(
text
)));
setTextEditingValue
(
TextEditingValue
(
text:
selection
.
textBefore
(
text
)
+
selection
.
textAfter
(
text
),
selection:
TextSelection
.
collapsed
(
offset:
math
.
min
(
selection
.
start
,
selection
.
end
),
affinity:
selection
.
affinity
,
),
),
cause
,
);
}
/// {@template flutter.widgets.TextEditingActionTarget.pasteText}
/// Paste text from [Clipboard].
/// {@endtemplate}
///
/// If there is currently a selection, it will be replaced.
///
/// If [readOnly] is true or the selection is invalid, does nothing.
///
/// {@macro flutter.widgets.TextEditingActionTarget.cause}
Future
<
void
>
pasteText
(
SelectionChangedCause
cause
)
async
{
final
TextSelection
selection
=
textEditingValue
.
selection
;
if
(
readOnly
||
!
selection
.
isValid
)
{
return
;
}
final
String
text
=
textEditingValue
.
text
;
assert
(
selection
!=
null
);
if
(!
selection
.
isValid
)
{
return
;
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final
ClipboardData
?
data
=
await
Clipboard
.
getData
(
Clipboard
.
kTextPlain
);
if
(
data
==
null
)
{
return
;
}
setTextEditingValue
(
TextEditingValue
(
text:
selection
.
textBefore
(
text
)
+
data
.
text
!
+
selection
.
textAfter
(
text
),
selection:
TextSelection
.
collapsed
(
offset:
math
.
min
(
selection
.
start
,
selection
.
end
)
+
data
.
text
!.
length
,
affinity:
selection
.
affinity
,
),
),
cause
,
);
}
}
packages/flutter/lib/src/widgets/text_editing_intents.dart
View file @
a1115b8b
...
...
@@ -62,8 +62,12 @@ class DeleteToLineBreakIntent extends DirectionalTextEditingIntent {
/// new location.
abstract
class
DirectionalCaretMovementIntent
extends
DirectionalTextEditingIntent
{
/// Creates a [DirectionalCaretMovementIntent].
const
DirectionalCaretMovementIntent
(
bool
forward
,
this
.
collapseSelection
)
:
super
(
forward
);
const
DirectionalCaretMovementIntent
(
bool
forward
,
this
.
collapseSelection
,
[
this
.
collapseAtReversal
=
false
]
)
:
assert
(!
collapseSelection
||
!
collapseAtReversal
),
super
(
forward
);
/// Whether this [Intent] should make the selection collapsed (so it becomes a
/// caret), after the movement.
...
...
@@ -76,6 +80,16 @@ abstract class DirectionalCaretMovementIntent extends DirectionalTextEditingInte
/// both the [TextSelection.base] and the [TextSelection.extent] to the new
/// location.
final
bool
collapseSelection
;
/// Whether to collapse the selection when it would otherwise reverse order.
///
/// For example, consider when forward is true and the extent is before the
/// base. If collapseAtReversal is true, then this will cause the selection to
/// collapse at the base. If it's false, then the extent will be placed at the
/// linebreak, reversing the order of base and offset.
///
/// Cannot be true when collapseSelection is true.
final
bool
collapseAtReversal
;
}
/// Expands, or moves the current selection from the current
...
...
@@ -126,7 +140,9 @@ class ExtendSelectionToLineBreakIntent extends DirectionalCaretMovementIntent {
const
ExtendSelectionToLineBreakIntent
({
required
bool
forward
,
required
bool
collapseSelection
,
})
:
super
(
forward
,
collapseSelection
);
bool
collapseAtReversal
=
false
,
})
:
assert
(!
collapseSelection
||
!
collapseAtReversal
),
super
(
forward
,
collapseSelection
,
collapseAtReversal
);
}
/// Expands, or moves the current selection from the current
...
...
packages/flutter/test/widgets/default_text_editing_actions_test.dart
deleted
100644 → 0
View file @
f62a1280
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
class
TestLeftIntent
extends
Intent
{
const
TestLeftIntent
();
}
class
TestRightIntent
extends
Intent
{
const
TestRightIntent
();
}
void
main
(
)
{
testWidgets
(
'DoNothingAndStopPropagationTextIntent'
,
(
WidgetTester
tester
)
async
{
bool
leftCalled
=
false
;
bool
rightCalled
=
false
;
final
TextEditingController
controller
=
TextEditingController
(
text:
'blah1 blah2'
,
);
final
FocusNode
focusNodeTarget
=
FocusNode
();
final
FocusNode
focusNodeNonTarget
=
FocusNode
();
await
tester
.
pumpWidget
(
MaterialApp
(
theme:
ThemeData
(),
home:
Scaffold
(
body:
Builder
(
builder:
(
BuildContext
context
)
{
return
Shortcuts
(
shortcuts:
const
<
ShortcutActivator
,
Intent
>{
SingleActivator
(
LogicalKeyboardKey
.
arrowLeft
):
TestLeftIntent
(),
SingleActivator
(
LogicalKeyboardKey
.
arrowRight
):
TestRightIntent
(),
},
child:
Shortcuts
(
shortcuts:
const
<
ShortcutActivator
,
Intent
>{
SingleActivator
(
LogicalKeyboardKey
.
arrowRight
):
DoNothingAndStopPropagationTextIntent
(),
},
child:
Actions
(
// These Actions intercept default Intents, set a flag that they
// were called, and then call through to the default Action.
actions:
<
Type
,
Action
<
Intent
>>{
TestLeftIntent:
CallbackAction
<
TestLeftIntent
>(
onInvoke:
(
Intent
intent
)
{
leftCalled
=
true
;
}),
TestRightIntent:
CallbackAction
<
TestRightIntent
>(
onInvoke:
(
Intent
intent
)
{
rightCalled
=
true
;
}),
},
child:
Center
(
child:
Column
(
children:
<
Widget
>[
EditableText
(
controller:
controller
,
focusNode:
focusNodeTarget
,
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
),
Focus
(
focusNode:
focusNodeNonTarget
,
child:
const
Text
(
'focusable'
),
),
],
),
),
),
),
);
},
),
),
));
// Focus on the EditableText, which is a TextEditingActionTarget.
focusNodeTarget
.
requestFocus
();
await
tester
.
pump
();
expect
(
focusNodeTarget
.
hasFocus
,
isTrue
);
expect
(
focusNodeNonTarget
.
hasFocus
,
isFalse
);
expect
(
controller
.
selection
.
isCollapsed
,
isTrue
);
expect
(
controller
.
selection
.
baseOffset
,
11
);
// The left arrow key's Action is called.
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowLeft
);
await
tester
.
pump
();
expect
(
leftCalled
,
isTrue
);
expect
(
rightCalled
,
isFalse
);
leftCalled
=
false
;
// The right arrow key is blocked by DoNothingAndStopPropagationTextIntent.
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowRight
);
await
tester
.
pump
();
expect
(
rightCalled
,
isFalse
);
expect
(
leftCalled
,
isFalse
);
// Focus on the other node, which is not a TextEditingActionTarget.
focusNodeNonTarget
.
requestFocus
();
await
tester
.
pump
();
expect
(
focusNodeTarget
.
hasFocus
,
isFalse
);
expect
(
focusNodeNonTarget
.
hasFocus
,
isTrue
);
// The left arrow key's Action is called as normal.
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowLeft
);
await
tester
.
pump
();
expect
(
leftCalled
,
isTrue
);
expect
(
rightCalled
,
isFalse
);
leftCalled
=
false
;
// The right arrow key's Action is also called. That's because
// DoNothingAndStopPropagationTextIntent only applies if a
// TextEditingActionTarget is currently focused.
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowRight
);
await
tester
.
pump
();
expect
(
leftCalled
,
isFalse
);
expect
(
rightCalled
,
isTrue
);
},
variant:
KeySimulatorTransitModeVariant
.
all
());
}
packages/flutter/test/widgets/editable_text_test.dart
View file @
a1115b8b
...
...
@@ -5556,6 +5556,271 @@ void main() {
variant:
TargetPlatformVariant
.
all
(),
);
testWidgets
(
'shift + home/end keys (Windows only)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
controller
.
selection
=
const
TextSelection
(
baseOffset:
0
,
extentOffset:
0
,
affinity:
TextAffinity
.
upstream
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Align
(
alignment:
Alignment
.
topLeft
,
child:
SizedBox
(
width:
400
,
child:
EditableText
(
maxLines:
10
,
controller:
controller
,
showSelectionHandles:
true
,
autofocus:
true
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
selectionControls:
materialTextSelectionControls
,
keyboardType:
TextInputType
.
text
,
textAlign:
TextAlign
.
right
,
),
),
),
));
await
tester
.
pump
();
// Move the selection away from the start so it can invert.
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
arrowRight
,
LogicalKeyboardKey
.
arrowRight
,
LogicalKeyboardKey
.
arrowRight
,
LogicalKeyboardKey
.
arrowRight
,
],
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
.
collapsed
(
offset:
4
,
)),
);
// Press shift + end and extend the selection to the end of the line.
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
end
,
],
shift:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
(
baseOffset:
4
,
extentOffset:
19
,
affinity:
TextAffinity
.
upstream
,
)),
);
// Press shift + home and the selection inverts and extends to the start, it
// does not collapse and stop at the inversion.
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
home
,
],
shift:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
(
baseOffset:
4
,
extentOffset:
0
,
)),
);
// Press shift + end again and the selection inverts and extends to the end,
// again it does not stop at the inversion.
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
end
,
],
shift:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
(
baseOffset:
4
,
extentOffset:
19
,
)),
);
},
skip:
kIsWeb
,
// [intended] on web these keys are handled by the browser.
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
windows
})
);
testWidgets
(
'control + home/end keys (Windows only)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
controller
.
selection
=
const
TextSelection
(
baseOffset:
0
,
extentOffset:
0
,
affinity:
TextAffinity
.
upstream
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Align
(
alignment:
Alignment
.
topLeft
,
child:
SizedBox
(
width:
400
,
child:
EditableText
(
maxLines:
10
,
controller:
controller
,
showSelectionHandles:
true
,
autofocus:
true
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
selectionControls:
materialTextSelectionControls
,
keyboardType:
TextInputType
.
text
,
textAlign:
TextAlign
.
right
,
),
),
),
));
await
tester
.
pump
();
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
end
,
],
shortcutModifier:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
.
collapsed
(
offset:
testText
.
length
,
affinity:
TextAffinity
.
upstream
,
)),
);
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
home
,
],
shortcutModifier:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
.
collapsed
(
offset:
0
)),
);
},
skip:
kIsWeb
,
// [intended] on web these keys are handled by the browser.
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
windows
})
);
testWidgets
(
'control + shift + home/end keys (Windows only)'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
(
text:
testText
);
controller
.
selection
=
const
TextSelection
(
baseOffset:
0
,
extentOffset:
0
,
affinity:
TextAffinity
.
upstream
,
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Align
(
alignment:
Alignment
.
topLeft
,
child:
SizedBox
(
width:
400
,
child:
EditableText
(
maxLines:
10
,
controller:
controller
,
showSelectionHandles:
true
,
autofocus:
true
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
subtitle1
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
selectionControls:
materialTextSelectionControls
,
keyboardType:
TextInputType
.
text
,
textAlign:
TextAlign
.
right
,
),
),
),
));
await
tester
.
pump
();
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
end
,
],
shortcutModifier:
true
,
shift:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
testText
.
length
,
)),
);
// Collapse the selection at the end.
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
arrowRight
,
],
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
.
collapsed
(
offset:
testText
.
length
,
affinity:
TextAffinity
.
upstream
,
)),
);
await
sendKeys
(
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
home
,
],
shortcutModifier:
true
,
shift:
true
,
targetPlatform:
defaultTargetPlatform
,
);
await
tester
.
pump
();
expect
(
controller
.
selection
,
equals
(
const
TextSelection
(
baseOffset:
testText
.
length
,
extentOffset:
0
,
)),
);
},
skip:
kIsWeb
,
// [intended] on web these keys are handled by the browser.
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
windows
})
);
// Regression test for https://github.com/flutter/flutter/issues/31287
testWidgets
(
'text selection handle visibility'
,
(
WidgetTester
tester
)
async
{
// Text with two separate words to select.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment