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
fc7bd6ad
Unverified
Commit
fc7bd6ad
authored
Jul 12, 2019
by
chunhtai
Committed by
GitHub
Jul 12, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor out selection handlers (#35207)
parent
e91822da
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
728 additions
and
280 deletions
+728
-280
text_field.dart
packages/flutter/lib/src/cupertino/text_field.dart
+52
-132
text_field.dart
packages/flutter/lib/src/material/text_field.dart
+121
-148
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+312
-0
text_selection_test.dart
packages/flutter/test/widgets/text_selection_test.dart
+243
-0
No files found.
packages/flutter/lib/src/cupertino/text_field.dart
View file @
fc7bd6ad
...
...
@@ -68,6 +68,39 @@ enum OverlayVisibilityMode {
always
,
}
class
_CupertinoTextFieldSelectionGestureDetectorBuilder
extends
TextSelectionGestureDetectorBuilder
{
_CupertinoTextFieldSelectionGestureDetectorBuilder
({
@required
_CupertinoTextFieldState
state
})
:
_state
=
state
,
super
(
delegate:
state
);
final
_CupertinoTextFieldState
_state
;
@override
void
onSingleTapUp
(
TapUpDetails
details
)
{
// Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger
// this handler. If the the clear button widget recognizes the up event,
// then do not handle it.
if
(
_state
.
_clearGlobalKey
.
currentContext
!=
null
)
{
final
RenderBox
renderBox
=
_state
.
_clearGlobalKey
.
currentContext
.
findRenderObject
();
final
Offset
localOffset
=
renderBox
.
globalToLocal
(
details
.
globalPosition
);
if
(
renderBox
.
hitTest
(
BoxHitTestResult
(),
position:
localOffset
))
{
return
;
}
}
super
.
onSingleTapUp
(
details
);
_state
.
_requestKeyboard
();
if
(
_state
.
widget
.
onTap
!=
null
)
_state
.
widget
.
onTap
();
}
@override
void
onDragSelectionEnd
(
DragEndDetails
details
)
{
_state
.
_requestKeyboard
();
}
}
/// An iOS-style text field.
///
/// A text field lets the user enter text, either with a hardware keyboard or with
...
...
@@ -506,9 +539,8 @@ class CupertinoTextField extends StatefulWidget {
}
}
class
_CupertinoTextFieldState
extends
State
<
CupertinoTextField
>
with
AutomaticKeepAliveClientMixin
{
class
_CupertinoTextFieldState
extends
State
<
CupertinoTextField
>
with
AutomaticKeepAliveClientMixin
implements
TextSelectionGestureDetectorBuilderDelegate
{
final
GlobalKey
_clearGlobalKey
=
GlobalKey
();
final
GlobalKey
<
EditableTextState
>
_editableTextKey
=
GlobalKey
<
EditableTextState
>();
TextEditingController
_controller
;
TextEditingController
get
_effectiveController
=>
widget
.
controller
??
_controller
;
...
...
@@ -516,17 +548,25 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
FocusNode
_focusNode
;
FocusNode
get
_effectiveFocusNode
=>
widget
.
focusNode
??
(
_focusNode
??=
FocusNode
());
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
// For backwards-compatibility, we treat a null kind the same as touch.
bool
_shouldShowSelectionToolbar
=
true
;
bool
_showSelectionHandles
=
false
;
_CupertinoTextFieldSelectionGestureDetectorBuilder
_selectionGestureDetectorBuilder
;
// API for TextSelectionGestureDetectorBuilderDelegate.
@override
bool
get
forcePressEnabled
=>
true
;
@override
final
GlobalKey
<
EditableTextState
>
editableTextKey
=
GlobalKey
<
EditableTextState
>();
@override
bool
get
selectionEnabled
=>
widget
.
selectionEnabled
;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
@override
void
initState
()
{
super
.
initState
();
_selectionGestureDetectorBuilder
=
_CupertinoTextFieldSelectionGestureDetectorBuilder
(
state:
this
);
if
(
widget
.
controller
==
null
)
{
_controller
=
TextEditingController
();
_controller
.
addListener
(
updateKeepAlive
);
...
...
@@ -556,103 +596,16 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
super
.
dispose
();
}
EditableTextState
get
_editableText
=>
_
editableTextKey
.
currentState
;
EditableTextState
get
_editableText
=>
editableTextKey
.
currentState
;
void
_requestKeyboard
()
{
_editableText
?.
requestKeyboard
();
}
RenderEditable
get
_renderEditable
=>
_editableText
.
renderEditable
;
void
_handleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
handleTapDown
(
details
);
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
// For backwards-compatibility, we treat a null kind the same as touch.
final
PointerDeviceKind
kind
=
details
.
kind
;
_shouldShowSelectionToolbar
=
kind
==
null
||
kind
==
PointerDeviceKind
.
touch
||
kind
==
PointerDeviceKind
.
stylus
;
}
void
_handleForcePressStarted
(
ForcePressDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
forcePress
,
);
}
}
void
_handleForcePressEnded
(
ForcePressDetails
details
)
{
_renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
forcePress
,
);
if
(
_shouldShowSelectionToolbar
)
_editableText
.
showToolbar
();
}
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
// Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger
// this handler. If the the clear button widget recognizes the up event,
// then do not handle it.
if
(
_clearGlobalKey
.
currentContext
!=
null
)
{
final
RenderBox
renderBox
=
_clearGlobalKey
.
currentContext
.
findRenderObject
();
final
Offset
localOffset
=
renderBox
.
globalToLocal
(
details
.
globalPosition
);
if
(
renderBox
.
hitTest
(
BoxHitTestResult
(),
position:
localOffset
))
{
return
;
}
}
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
}
_requestKeyboard
();
if
(
widget
.
onTap
!=
null
)
{
widget
.
onTap
();
}
}
void
_handleSingleLongTapStart
(
LongPressStartDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
}
}
void
_handleSingleLongTapMoveUpdate
(
LongPressMoveUpdateDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
}
}
void
_handleSingleLongTapEnd
(
LongPressEndDetails
details
)
{
if
(
_shouldShowSelectionToolbar
)
_editableText
.
showToolbar
();
}
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
tap
);
if
(
_shouldShowSelectionToolbar
)
_editableText
.
showToolbar
();
}
}
bool
_shouldShowSelectionHandles
(
SelectionChangedCause
cause
)
{
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
if
(!
_shouldShowSelectionToolbar
)
if
(!
_s
electionGestureDetectorBuilder
.
s
houldShowSelectionToolbar
)
return
false
;
// On iOS, we don't show handles when the selection is collapsed.
...
...
@@ -668,28 +621,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
return
false
;
}
void
_handleMouseDragSelectionStart
(
DragStartDetails
details
)
{
_renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
drag
,
);
}
void
_handleMouseDragSelectionUpdate
(
DragStartDetails
startDetails
,
DragUpdateDetails
updateDetails
,
)
{
_renderEditable
.
selectPositionAt
(
from:
startDetails
.
globalPosition
,
to:
updateDetails
.
globalPosition
,
cause:
SelectionChangedCause
.
drag
,
);
}
void
_handleMouseDragSelectionEnd
(
DragEndDetails
details
)
{
_requestKeyboard
();
}
void
_handleSelectionChanged
(
TextSelection
selection
,
SelectionChangedCause
cause
)
{
if
(
cause
==
SelectionChangedCause
.
longPress
)
{
_editableText
?.
bringIntoView
(
selection
.
base
);
...
...
@@ -870,7 +801,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
padding:
widget
.
padding
,
child:
RepaintBoundary
(
child:
EditableText
(
key:
_
editableTextKey
,
key:
editableTextKey
,
controller:
controller
,
readOnly:
widget
.
readOnly
,
showCursor:
widget
.
showCursor
,
...
...
@@ -925,18 +856,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
ignoring:
!
enabled
,
child:
Container
(
decoration:
effectiveDecoration
,
child:
TextSelectionGestureDetector
(
onTapDown:
_handleTapDown
,
onForcePressStart:
_handleForcePressStarted
,
onForcePressEnd:
_handleForcePressEnded
,
onSingleTapUp:
_handleSingleTapUp
,
onSingleLongTapStart:
_handleSingleLongTapStart
,
onSingleLongTapMoveUpdate:
_handleSingleLongTapMoveUpdate
,
onSingleLongTapEnd:
_handleSingleLongTapEnd
,
onDoubleTapDown:
_handleDoubleTapDown
,
onDragSelectionStart:
_handleMouseDragSelectionStart
,
onDragSelectionUpdate:
_handleMouseDragSelectionUpdate
,
onDragSelectionEnd:
_handleMouseDragSelectionEnd
,
child:
_selectionGestureDetectorBuilder
.
buildGestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
child:
Align
(
alignment:
Alignment
(-
1.0
,
_textAlignVertical
.
y
),
...
...
packages/flutter/lib/src/material/text_field.dart
View file @
fc7bd6ad
...
...
@@ -35,6 +35,107 @@ typedef InputCounterWidgetBuilder = Widget Function(
@required
bool
isFocused
,
});
class
_TextFieldSelectionGestureDetectorBuilder
extends
TextSelectionGestureDetectorBuilder
{
_TextFieldSelectionGestureDetectorBuilder
({
@required
_TextFieldState
state
})
:
_state
=
state
,
super
(
delegate:
state
);
final
_TextFieldState
_state
;
@override
void
onTapDown
(
TapDownDetails
details
)
{
super
.
onTapDown
(
details
);
_state
.
_startSplash
(
details
.
globalPosition
);
}
@override
void
onForcePressStart
(
ForcePressDetails
details
)
{
super
.
onForcePressStart
(
details
);
if
(
delegate
.
selectionEnabled
&&
shouldShowSelectionToolbar
)
{
editableText
.
showToolbar
();
}
}
@override
void
onForcePressEnd
(
ForcePressDetails
details
)
{
// Not required.
}
@override
void
onSingleLongTapMoveUpdate
(
LongPressMoveUpdateDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
_state
.
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
-
details
.
offsetFromOrigin
,
to:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
}
}
}
@override
void
onSingleTapUp
(
TapUpDetails
details
)
{
editableText
.
hideToolbar
();
if
(
delegate
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
_state
.
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
tap
);
break
;
}
}
_state
.
_requestKeyboard
();
_state
.
_confirmCurrentSplash
();
if
(
_state
.
widget
.
onTap
!=
null
)
_state
.
widget
.
onTap
();
}
@override
void
onSingleTapCancel
()
{
_state
.
_cancelCurrentSplash
();
}
@override
void
onSingleLongTapStart
(
LongPressStartDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
_state
.
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
longPress
);
Feedback
.
forLongPress
(
_state
.
context
);
break
;
}
}
_state
.
_confirmCurrentSplash
();
}
@override
void
onDragSelectionStart
(
DragStartDetails
details
)
{
super
.
onDragSelectionStart
(
details
);
_state
.
_startSplash
(
details
.
globalPosition
);
}
}
/// A material design text field.
///
/// A text field lets the user enter text, either with hardware keyboard or with
...
...
@@ -531,9 +632,7 @@ class TextField extends StatefulWidget {
}
}
class
_TextFieldState
extends
State
<
TextField
>
with
AutomaticKeepAliveClientMixin
{
final
GlobalKey
<
EditableTextState
>
_editableTextKey
=
GlobalKey
<
EditableTextState
>();
class
_TextFieldState
extends
State
<
TextField
>
with
AutomaticKeepAliveClientMixin
implements
TextSelectionGestureDetectorBuilderDelegate
{
Set
<
InteractiveInkFeature
>
_splashes
;
InteractiveInkFeature
_currentSplash
;
...
...
@@ -549,10 +648,21 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
&&
widget
.
decoration
!=
null
&&
widget
.
decoration
.
counterText
==
null
;
bool
_shouldShowSelectionToolbar
=
true
;
bool
_showSelectionHandles
=
false
;
_TextFieldSelectionGestureDetectorBuilder
_selectionGestureDetectorBuilder
;
// API for TextSelectionGestureDetectorBuilderDelegate.
@override
bool
forcePressEnabled
;
@override
final
GlobalKey
<
EditableTextState
>
editableTextKey
=
GlobalKey
<
EditableTextState
>();
@override
bool
get
selectionEnabled
=>
widget
.
selectionEnabled
;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
InputDecoration
_getEffectiveDecoration
()
{
final
MaterialLocalizations
localizations
=
MaterialLocalizations
.
of
(
context
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
...
...
@@ -621,6 +731,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
@override
void
initState
()
{
super
.
initState
();
_selectionGestureDetectorBuilder
=
_TextFieldSelectionGestureDetectorBuilder
(
state:
this
);
if
(
widget
.
controller
==
null
)
_controller
=
TextEditingController
();
}
...
...
@@ -650,7 +761,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
super
.
dispose
();
}
EditableTextState
get
_editableText
=>
_
editableTextKey
.
currentState
;
EditableTextState
get
_editableText
=>
editableTextKey
.
currentState
;
void
_requestKeyboard
()
{
_editableText
?.
requestKeyboard
();
...
...
@@ -659,7 +770,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
bool
_shouldShowSelectionHandles
(
SelectionChangedCause
cause
)
{
// When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either.
if
(!
_shouldShowSelectionToolbar
)
if
(!
_s
electionGestureDetectorBuilder
.
s
houldShowSelectionToolbar
)
return
false
;
if
(
cause
==
SelectionChangedCause
.
keyboard
)
...
...
@@ -707,7 +818,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
InteractiveInkFeature
_createInkFeature
(
Offset
globalPosition
)
{
final
MaterialInkController
inkController
=
Material
.
of
(
context
);
final
ThemeData
themeData
=
Theme
.
of
(
context
);
final
BuildContext
editableContext
=
_
editableTextKey
.
currentContext
;
final
BuildContext
editableContext
=
editableTextKey
.
currentContext
;
final
RenderBox
referenceBox
=
InputDecorator
.
containerOf
(
editableContext
)
??
editableContext
.
findRenderObject
();
final
Offset
position
=
referenceBox
.
globalToLocal
(
globalPosition
);
final
Color
color
=
themeData
.
splashColor
;
...
...
@@ -738,133 +849,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
return
splash
;
}
RenderEditable
get
_renderEditable
=>
_editableTextKey
.
currentState
.
renderEditable
;
void
_handleTapDown
(
TapDownDetails
details
)
{
_renderEditable
.
handleTapDown
(
details
);
_startSplash
(
details
.
globalPosition
);
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
// For backwards-compatibility, we treat a null kind the same as touch.
final
PointerDeviceKind
kind
=
details
.
kind
;
_shouldShowSelectionToolbar
=
kind
==
null
||
kind
==
PointerDeviceKind
.
touch
||
kind
==
PointerDeviceKind
.
stylus
;
}
void
_handleForcePressStarted
(
ForcePressDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
forcePress
,
);
if
(
_shouldShowSelectionToolbar
)
{
_editableTextKey
.
currentState
.
showToolbar
();
}
}
}
void
_handleSingleTapUp
(
TapUpDetails
details
)
{
_editableTextKey
.
currentState
.
hideToolbar
();
if
(
widget
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
_renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
_renderEditable
.
selectPosition
(
cause:
SelectionChangedCause
.
tap
);
break
;
}
}
_requestKeyboard
();
_confirmCurrentSplash
();
if
(
widget
.
onTap
!=
null
)
widget
.
onTap
();
}
void
_handleSingleTapCancel
()
{
_cancelCurrentSplash
();
}
void
_handleSingleLongTapStart
(
LongPressStartDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
_renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
longPress
);
Feedback
.
forLongPress
(
context
);
break
;
}
}
_confirmCurrentSplash
();
}
void
_handleSingleLongTapMoveUpdate
(
LongPressMoveUpdateDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
switch
(
Theme
.
of
(
context
).
platform
)
{
case
TargetPlatform
.
iOS
:
_renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
case
TargetPlatform
.
android
:
case
TargetPlatform
.
fuchsia
:
_renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
-
details
.
offsetFromOrigin
,
to:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
break
;
}
}
}
void
_handleSingleLongTapEnd
(
LongPressEndDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
if
(
_shouldShowSelectionToolbar
)
_editableTextKey
.
currentState
.
showToolbar
();
}
}
void
_handleDoubleTapDown
(
TapDownDetails
details
)
{
if
(
widget
.
selectionEnabled
)
{
_renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
doubleTap
);
if
(
_shouldShowSelectionToolbar
)
{
_editableText
.
showToolbar
();
}
}
}
void
_handleMouseDragSelectionStart
(
DragStartDetails
details
)
{
_renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
drag
,
);
_startSplash
(
details
.
globalPosition
);
}
void
_handleMouseDragSelectionUpdate
(
DragStartDetails
startDetails
,
DragUpdateDetails
updateDetails
,
)
{
_renderEditable
.
selectPositionAt
(
from:
startDetails
.
globalPosition
,
to:
updateDetails
.
globalPosition
,
cause:
SelectionChangedCause
.
drag
,
);
}
void
_startSplash
(
Offset
globalPosition
)
{
if
(
_effectiveFocusNode
.
hasFocus
)
return
;
...
...
@@ -933,7 +917,6 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
if
(
widget
.
maxLength
!=
null
&&
widget
.
maxLengthEnforced
)
formatters
.
add
(
LengthLimitingTextInputFormatter
(
widget
.
maxLength
));
bool
forcePressEnabled
;
TextSelectionControls
textSelectionControls
;
bool
paintCursorAboveText
;
bool
cursorOpacityAnimates
;
...
...
@@ -971,7 +954,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
Widget
child
=
RepaintBoundary
(
child:
EditableText
(
key:
_
editableTextKey
,
key:
editableTextKey
,
readOnly:
widget
.
readOnly
,
showCursor:
widget
.
showCursor
,
showSelectionHandles:
_showSelectionHandles
,
...
...
@@ -1046,17 +1029,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
onPointerExit:
_handlePointerExit
,
child:
IgnorePointer
(
ignoring:
!(
widget
.
enabled
??
widget
.
decoration
?.
enabled
??
true
),
child:
TextSelectionGestureDetector
(
onTapDown:
_handleTapDown
,
onForcePressStart:
forcePressEnabled
?
_handleForcePressStarted
:
null
,
onSingleTapUp:
_handleSingleTapUp
,
onSingleTapCancel:
_handleSingleTapCancel
,
onSingleLongTapStart:
_handleSingleLongTapStart
,
onSingleLongTapMoveUpdate:
_handleSingleLongTapMoveUpdate
,
onSingleLongTapEnd:
_handleSingleLongTapEnd
,
onDoubleTapDown:
_handleDoubleTapDown
,
onDragSelectionStart:
_handleMouseDragSelectionStart
,
onDragSelectionUpdate:
_handleMouseDragSelectionUpdate
,
child:
_selectionGestureDetectorBuilder
.
buildGestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
child:
child
,
),
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
fc7bd6ad
...
...
@@ -813,6 +813,318 @@ class _TextSelectionHandleOverlayState
}
}
/// Delegate interface for the [TextSelectionGestureDetectorBuilder].
///
/// The interface is usually implemented by textfield implementations wrapping
/// [EditableText], that use a [TextSelectionGestureDetectorBuilder] to build a
/// [TextSelectionGestureDetector] for their [EditableText]. The delegate provides
/// the builder with information about the current state of the textfield.
/// Based on these information, the builder adds the correct gesture handlers
/// to the gesture detector.
///
/// See also:
///
/// * [TextField], which implements this delegate for the Material textfield.
/// * [CupertinoTextField], which implements this delegate for the Cupertino textfield.
abstract
class
TextSelectionGestureDetectorBuilderDelegate
{
/// [GlobalKey] to the [EditableText] for which the
/// [TextSelectionGestureDetectorBuilder] will build a [TextSelectionGestureDetector].
GlobalKey
<
EditableTextState
>
get
editableTextKey
;
/// Whether the textfield should respond to force presses.
bool
get
forcePressEnabled
;
/// Whether the user may select text in the textfield.
bool
get
selectionEnabled
;
}
/// Builds a [TextSelectionGestureDetector] to wrap an [EditableText].
///
/// The class implements sensible defaults for many user interactions
/// with an [EditableText] (see the documentation of the various gesture handler
/// methods, e.g. [onTapDown], [onFrocePress], etc.). Subclasses of
/// [EditableTextSelectionHandlesProvider] can change the behavior performed in
/// responds to these gesture events by overriding the corresponding handler
/// methods of this class.
///
/// The resulting [TextSelectionGestureDetector] to wrap an [EditableText] is
/// obtained by calling [buildGestureDetector].
///
/// See also:
///
/// * [TextField], which uses a subclass to implement the Material-specific
/// gesture logic of an [EditableText].
/// * [CupertinoTextField], which uses a subclass to implement the
/// Cupertino-specific gesture logic of an [EditableText].
class
TextSelectionGestureDetectorBuilder
{
/// Creates a [TextSelectionGestureDetectorBuilder].
///
/// The [delegate] must not be null.
TextSelectionGestureDetectorBuilder
({
@required
this
.
delegate
,
})
:
assert
(
delegate
!=
null
);
/// The delegate for this [TextSelectionGestureDetectorBuilder].
///
/// The delegate provides the builder with information about what actions can
/// currently be performed on the textfield. Based on this, the builder adds
/// the correct gesture handlers to the gesture detector.
@protected
final
TextSelectionGestureDetectorBuilderDelegate
delegate
;
/// Whether to show the selection tool bar.
///
/// It is based on the signal source when a [onTapDown] is called. This getter
/// will return true if current [onTapDown] event is triggered by a touch or
/// a stylus.
bool
get
shouldShowSelectionToolbar
=>
_shouldShowSelectionToolbar
;
bool
_shouldShowSelectionToolbar
=
true
;
/// The [State] of the [EditableText] for which the builder will provide a
/// [TextSelectionGestureDetector].
@protected
EditableTextState
get
editableText
=>
delegate
.
editableTextKey
.
currentState
;
/// The [RenderObject] of the [EditableText] for which the builder will
/// provide a [TextSelectionGestureDetector].
@protected
RenderEditable
get
renderEditable
=>
editableText
.
renderEditable
;
/// Handler for [TextSelectionGestureDetector.onTapDown].
///
/// By default, it forwards the tap to [RenderEditable.handleTapDown] and sets
/// [shouldShowSelectionToolbar] to true if the tap was initiated by a finger or stylus.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onTapDown], which triggers this callback.
@protected
void
onTapDown
(
TapDownDetails
details
)
{
renderEditable
.
handleTapDown
(
details
);
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
// For backwards-compatibility, we treat a null kind the same as touch.
final
PointerDeviceKind
kind
=
details
.
kind
;
_shouldShowSelectionToolbar
=
kind
==
null
||
kind
==
PointerDeviceKind
.
touch
||
kind
==
PointerDeviceKind
.
stylus
;
}
/// Handler for [TextSelectionGestureDetector.onForcePressStart].
///
/// By default, it selects the word at the position of the force press,
/// if selection is enabled.
///
/// This callback is only applicable when force press is enabled.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onForcePressStart], which triggers this
/// callback.
@protected
void
onForcePressStart
(
ForcePressDetails
details
)
{
assert
(
delegate
.
forcePressEnabled
);
_shouldShowSelectionToolbar
=
true
;
if
(
delegate
.
selectionEnabled
)
{
renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
forcePress
,
);
}
}
/// Handler for [TextSelectionGestureDetector.onForcePressEnd].
///
/// By default, it selects words in the range specified in [details] and shows
/// tool bar if it is necessary.
///
/// This callback is only applicable when force press is enabled.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onForcePressEnd], which triggers this
/// callback.
@protected
void
onForcePressEnd
(
ForcePressDetails
details
)
{
assert
(
delegate
.
forcePressEnabled
);
renderEditable
.
selectWordsInRange
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
forcePress
,
);
if
(
shouldShowSelectionToolbar
)
editableText
.
showToolbar
();
}
/// Handler for [TextSelectionGestureDetector.onSingleTapUp].
///
/// By default, it selects word edge if selection is enabled.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onSingleTapUp], which triggers
/// this callback.
@protected
void
onSingleTapUp
(
TapUpDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
renderEditable
.
selectWordEdge
(
cause:
SelectionChangedCause
.
tap
);
}
}
/// Handler for [TextSelectionGestureDetector.onSingleTapCancel].
///
/// By default, it services as place holder to enable subclass override.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onSingleTapCancel], which triggers
/// this callback.
@protected
void
onSingleTapCancel
()
{
/* Subclass should override this method if needed. */
}
/// Handler for [TextSelectionGestureDetector.onSingleLongTapStart].
///
/// By default, it selects text position specified in [details] if selection
/// is enabled.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onSingleLongTapStart], which triggers
/// this callback.
@protected
void
onSingleLongTapStart
(
LongPressStartDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
}
}
/// Handler for [TextSelectionGestureDetector.onSingleLongTapMoveUpdate].
///
/// By default, it updates the selection location specified in [details] if
/// selection is enabled.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onSingleLongTapMoveUpdate], which
/// triggers this callback.
@protected
void
onSingleLongTapMoveUpdate
(
LongPressMoveUpdateDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
longPress
,
);
}
}
/// Handler for [TextSelectionGestureDetector.onSingleLongTapEnd].
///
/// By default, it shows tool bar if necessary.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onSingleLongTapEnd], which triggers this
/// callback.
@protected
void
onSingleLongTapEnd
(
LongPressEndDetails
details
)
{
if
(
shouldShowSelectionToolbar
)
editableText
.
showToolbar
();
}
/// Handler for [TextSelectionGestureDetector.onDoubleTapDown].
///
/// By default, it selects a word through [renderEditable.selectWord] if
/// selectionEnabled and shows tool bar if necessary.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDoubleTapDown], which triggers this
/// callback.
@protected
void
onDoubleTapDown
(
TapDownDetails
details
)
{
if
(
delegate
.
selectionEnabled
)
{
renderEditable
.
selectWord
(
cause:
SelectionChangedCause
.
tap
);
if
(
shouldShowSelectionToolbar
)
editableText
.
showToolbar
();
}
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionStart].
///
/// By default, it selects a text position specified in [details].
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDragSelectionStart], which triggers
/// this callback.
@protected
void
onDragSelectionStart
(
DragStartDetails
details
)
{
renderEditable
.
selectPositionAt
(
from:
details
.
globalPosition
,
cause:
SelectionChangedCause
.
drag
,
);
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionUpdate].
///
/// By default, it updates the selection location specified in [details].
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDragSelectionUpdate], which triggers
/// this callback./lib/src/material/text_field.dart
@protected
void
onDragSelectionUpdate
(
DragStartDetails
startDetails
,
DragUpdateDetails
updateDetails
)
{
renderEditable
.
selectPositionAt
(
from:
startDetails
.
globalPosition
,
to:
updateDetails
.
globalPosition
,
cause:
SelectionChangedCause
.
drag
,
);
}
/// Handler for [TextSelectionGestureDetector.onDragSelectionEnd].
///
/// By default, it services as place holder to enable subclass override.
///
/// See also:
///
/// * [TextSelectionGestureDetector.onDragSelectionEnd], which triggers this
/// callback.
@protected
void
onDragSelectionEnd
(
DragEndDetails
details
)
{
/* Subclass should override this method if needed. */
}
/// Returns a [TextSelectionGestureDetector] configured with the handlers
/// provided by this builder.
///
/// The [child] or its subtree should contain [EditableText].
Widget
buildGestureDetector
({
Key
key
,
HitTestBehavior
behavior
,
Widget
child
})
{
return
TextSelectionGestureDetector
(
key:
key
,
onTapDown:
onTapDown
,
onForcePressStart:
delegate
.
forcePressEnabled
?
onForcePressStart
:
null
,
onForcePressEnd:
delegate
.
forcePressEnabled
?
onForcePressEnd
:
null
,
onSingleTapUp:
onSingleTapUp
,
onSingleTapCancel:
onSingleTapCancel
,
onSingleLongTapStart:
onSingleLongTapStart
,
onSingleLongTapMoveUpdate:
onSingleLongTapMoveUpdate
,
onSingleLongTapEnd:
onSingleLongTapEnd
,
onDoubleTapDown:
onDoubleTapDown
,
onDragSelectionStart:
onDragSelectionStart
,
onDragSelectionUpdate:
onDragSelectionUpdate
,
onDragSelectionEnd:
onDragSelectionEnd
,
behavior:
behavior
,
child:
child
,
);
}
}
/// A gesture detector to respond to non-exclusive event chains for a text field.
///
/// An ordinary [GestureDetector] configured to handle events like tap and
...
...
packages/flutter/test/widgets/text_selection_test.dart
View file @
fc7bd6ad
...
...
@@ -5,6 +5,8 @@
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter/gestures.dart'
show
PointerDeviceKind
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/material.dart'
;
void
main
(
)
{
int
tapCount
;
...
...
@@ -62,6 +64,30 @@ void main() {
);
}
Future
<
void
>
pumpTextSelectionGestureDetectorBuilder
(
WidgetTester
tester
,
{
bool
forcePressEnabled
=
true
,
bool
selectionEnabled
=
true
,
})
async
{
final
GlobalKey
<
EditableTextState
>
editableTextKey
=
GlobalKey
<
EditableTextState
>();
final
FakeTextSelectionGestureDetectorBuilderDelegate
delegate
=
FakeTextSelectionGestureDetectorBuilderDelegate
(
editableTextKey:
editableTextKey
,
forcePressEnabled:
forcePressEnabled
,
selectionEnabled:
selectionEnabled
,
);
final
TextSelectionGestureDetectorBuilder
provider
=
TextSelectionGestureDetectorBuilder
(
delegate:
delegate
);
await
tester
.
pumpWidget
(
MaterialApp
(
home:
provider
.
buildGestureDetector
(
behavior:
HitTestBehavior
.
translucent
,
child:
FakeEditableText
(
key:
editableTextKey
)
)
)
);
}
testWidgets
(
'a series of taps all call onTaps'
,
(
WidgetTester
tester
)
async
{
await
pumpGestureDetector
(
tester
);
await
tester
.
tapAt
(
const
Offset
(
200
,
200
));
...
...
@@ -380,4 +406,221 @@ void main() {
await
gesture
.
removePointer
();
});
testWidgets
(
'test TextSelectionGestureDetectorBuilder long press'
,
(
WidgetTester
tester
)
async
{
await
pumpTextSelectionGestureDetectorBuilder
(
tester
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200.0
,
200.0
),
pointer:
0
,
kind:
PointerDeviceKind
.
touch
);
await
tester
.
pump
(
const
Duration
(
seconds:
2
));
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
final
FakeEditableTextState
state
=
tester
.
state
(
find
.
byType
(
FakeEditableText
));
final
FakeRenderEditable
renderEditable
=
tester
.
renderObject
(
find
.
byType
(
FakeEditable
));
expect
(
state
.
showToolbarCalled
,
isTrue
);
expect
(
renderEditable
.
selectPositionAtCalled
,
isTrue
);
});
testWidgets
(
'test TextSelectionGestureDetectorBuilder tap'
,
(
WidgetTester
tester
)
async
{
await
pumpTextSelectionGestureDetectorBuilder
(
tester
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200.0
,
200.0
),
pointer:
0
,
kind:
PointerDeviceKind
.
touch
);
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
final
FakeEditableTextState
state
=
tester
.
state
(
find
.
byType
(
FakeEditableText
));
final
FakeRenderEditable
renderEditable
=
tester
.
renderObject
(
find
.
byType
(
FakeEditable
));
expect
(
state
.
showToolbarCalled
,
isFalse
);
expect
(
renderEditable
.
selectWordEdgeCalled
,
isTrue
);
});
testWidgets
(
'test TextSelectionGestureDetectorBuilder double tap'
,
(
WidgetTester
tester
)
async
{
await
pumpTextSelectionGestureDetectorBuilder
(
tester
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200.0
,
200.0
),
pointer:
0
,
kind:
PointerDeviceKind
.
touch
);
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
gesture
.
up
();
await
gesture
.
down
(
const
Offset
(
200.0
,
200.0
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
final
FakeEditableTextState
state
=
tester
.
state
(
find
.
byType
(
FakeEditableText
));
final
FakeRenderEditable
renderEditable
=
tester
.
renderObject
(
find
.
byType
(
FakeEditable
));
expect
(
state
.
showToolbarCalled
,
isTrue
);
expect
(
renderEditable
.
selectWordCalled
,
isTrue
);
});
testWidgets
(
'test TextSelectionGestureDetectorBuilder forcePress enabled'
,
(
WidgetTester
tester
)
async
{
await
pumpTextSelectionGestureDetectorBuilder
(
tester
);
final
TestGesture
gesture
=
await
tester
.
createGesture
();
await
gesture
.
downWithCustomEvent
(
const
Offset
(
200.0
,
200.0
),
const
PointerDownEvent
(
pointer:
0
,
position:
Offset
(
200.0
,
200.0
),
pressure:
3.0
,
pressureMax:
6.0
,
pressureMin:
0.0
,
),
);
await
gesture
.
updateWithCustomEvent
(
const
PointerUpEvent
(
pointer:
0
,
position:
Offset
(
200.0
,
200.0
),
pressure:
0.0
,
pressureMax:
6.0
,
pressureMin:
0.0
,
),
);
await
tester
.
pump
();
final
FakeEditableTextState
state
=
tester
.
state
(
find
.
byType
(
FakeEditableText
));
final
FakeRenderEditable
renderEditable
=
tester
.
renderObject
(
find
.
byType
(
FakeEditable
));
expect
(
state
.
showToolbarCalled
,
isTrue
);
expect
(
renderEditable
.
selectWordsInRangeCalled
,
isTrue
);
});
testWidgets
(
'test TextSelectionGestureDetectorBuilder selection disabled'
,
(
WidgetTester
tester
)
async
{
await
pumpTextSelectionGestureDetectorBuilder
(
tester
,
selectionEnabled:
false
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
const
Offset
(
200.0
,
200.0
),
pointer:
0
,
kind:
PointerDeviceKind
.
touch
);
await
tester
.
pump
(
const
Duration
(
seconds:
2
));
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
final
FakeEditableTextState
state
=
tester
.
state
(
find
.
byType
(
FakeEditableText
));
final
FakeRenderEditable
renderEditable
=
tester
.
renderObject
(
find
.
byType
(
FakeEditable
));
expect
(
state
.
showToolbarCalled
,
isTrue
);
expect
(
renderEditable
.
selectWordsInRangeCalled
,
isFalse
);
});
testWidgets
(
'test TextSelectionGestureDetectorBuilder forcePress disabled'
,
(
WidgetTester
tester
)
async
{
await
pumpTextSelectionGestureDetectorBuilder
(
tester
,
forcePressEnabled:
false
);
final
TestGesture
gesture
=
await
tester
.
createGesture
();
await
gesture
.
downWithCustomEvent
(
const
Offset
(
200.0
,
200.0
),
const
PointerDownEvent
(
pointer:
0
,
position:
Offset
(
200.0
,
200.0
),
pressure:
3.0
,
pressureMax:
6.0
,
pressureMin:
0.0
,
),
);
await
gesture
.
up
();
await
tester
.
pump
();
final
FakeEditableTextState
state
=
tester
.
state
(
find
.
byType
(
FakeEditableText
));
final
FakeRenderEditable
renderEditable
=
tester
.
renderObject
(
find
.
byType
(
FakeEditable
));
expect
(
state
.
showToolbarCalled
,
isFalse
);
expect
(
renderEditable
.
selectWordsInRangeCalled
,
isFalse
);
});
}
class
FakeTextSelectionGestureDetectorBuilderDelegate
implements
TextSelectionGestureDetectorBuilderDelegate
{
FakeTextSelectionGestureDetectorBuilderDelegate
({
this
.
editableTextKey
,
this
.
forcePressEnabled
,
this
.
selectionEnabled
,
});
@override
final
GlobalKey
<
EditableTextState
>
editableTextKey
;
@override
final
bool
forcePressEnabled
;
@override
final
bool
selectionEnabled
;
}
class
FakeEditableText
extends
EditableText
{
FakeEditableText
({
Key
key
}):
super
(
key:
key
,
controller:
TextEditingController
(),
focusNode:
FocusNode
(),
backgroundCursorColor:
Colors
.
white
,
cursorColor:
Colors
.
white
,
style:
const
TextStyle
(),
);
@override
FakeEditableTextState
createState
()
=>
FakeEditableTextState
();
}
class
FakeEditableTextState
extends
EditableTextState
{
final
GlobalKey
_editableKey
=
GlobalKey
();
bool
showToolbarCalled
=
false
;
@override
RenderEditable
get
renderEditable
=>
_editableKey
.
currentContext
.
findRenderObject
();
@override
bool
showToolbar
()
{
showToolbarCalled
=
true
;
return
true
;
}
@override
Widget
build
(
BuildContext
context
)
{
super
.
build
(
context
);
return
FakeEditable
(
this
,
key:
_editableKey
);
}
}
class
FakeEditable
extends
LeafRenderObjectWidget
{
const
FakeEditable
(
this
.
delegate
,
{
Key
key
,
})
:
super
(
key:
key
);
final
EditableTextState
delegate
;
@override
RenderEditable
createRenderObject
(
BuildContext
context
)
{
return
FakeRenderEditable
(
delegate
);
}
}
class
FakeRenderEditable
extends
RenderEditable
{
FakeRenderEditable
(
EditableTextState
delegate
)
:
super
(
text:
const
TextSpan
(
style:
TextStyle
(
height:
1.0
,
fontSize:
10.0
,
fontFamily:
'Ahem'
),
text:
'placeholder'
,
),
startHandleLayerLink:
LayerLink
(),
endHandleLayerLink:
LayerLink
(),
textAlign:
TextAlign
.
start
,
textDirection:
TextDirection
.
ltr
,
locale:
const
Locale
(
'en'
,
'US'
),
offset:
ViewportOffset
.
fixed
(
10.0
),
textSelectionDelegate:
delegate
,
selection:
const
TextSelection
.
collapsed
(
offset:
0
,
),
);
bool
selectWordsInRangeCalled
=
false
;
@override
void
selectWordsInRange
({
@required
Offset
from
,
Offset
to
,
@required
SelectionChangedCause
cause
})
{
selectWordsInRangeCalled
=
true
;
}
bool
selectWordEdgeCalled
=
false
;
@override
void
selectWordEdge
({
@required
SelectionChangedCause
cause
})
{
selectWordEdgeCalled
=
true
;
}
bool
selectPositionAtCalled
=
false
;
@override
void
selectPositionAt
({
@required
Offset
from
,
Offset
to
,
@required
SelectionChangedCause
cause
})
{
selectPositionAtCalled
=
true
;
}
bool
selectWordCalled
=
false
;
@override
void
selectWord
({
@required
SelectionChangedCause
cause
})
{
selectWordCalled
=
true
;
}
}
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