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
57d66664
Commit
57d66664
authored
May 06, 2019
by
yaheng
Committed by
LongCatIsLooong
May 06, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix text selection toolbar appearing under obstructions (#29809)
parent
cc7ec6d6
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
279 additions
and
31 deletions
+279
-31
text_selection.dart
packages/flutter/lib/src/cupertino/text_selection.dart
+83
-25
text_selection.dart
packages/flutter/lib/src/material/text_selection.dart
+28
-3
text_selection.dart
packages/flutter/lib/src/widgets/text_selection.dart
+21
-2
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+70
-0
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+76
-0
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+1
-1
No files found.
packages/flutter/lib/src/cupertino/text_selection.dart
View file @
57d66664
...
...
@@ -41,17 +41,33 @@ const TextStyle _kToolbarButtonFontStyle = TextStyle(
color:
CupertinoColors
.
white
,
);
/// The direction of the triangle attached to the toolbar.
///
/// Defaults to showing the triangle downwards if sufficient space is available
/// to show the toolbar above the text field. Otherwise, the toolbar will
/// appear below the text field and the triangle's direction will be [up].
enum
_ArrowDirection
{
up
,
down
}
/// Paints a triangle below the toolbar.
class
_TextSelectionToolbarNotchPainter
extends
CustomPainter
{
const
_TextSelectionToolbarNotchPainter
(
this
.
arrowDirection
)
:
assert
(
arrowDirection
!=
null
);
final
_ArrowDirection
arrowDirection
;
@override
void
paint
(
Canvas
canvas
,
Size
size
)
{
final
Paint
paint
=
Paint
()
..
color
=
_kToolbarBackgroundColor
..
style
=
PaintingStyle
.
fill
;
final
double
triangleBottomY
=
(
arrowDirection
==
_ArrowDirection
.
down
)
?
0.0
:
_kToolbarTriangleSize
.
height
;
final
Path
triangle
=
Path
()
..
lineTo
(
_kToolbarTriangleSize
.
width
/
2
,
0.0
)
..
lineTo
(
_kToolbarTriangleSize
.
width
/
2
,
triangleBottomY
)
..
lineTo
(
0.0
,
_kToolbarTriangleSize
.
height
)
..
lineTo
(-(
_kToolbarTriangleSize
.
width
/
2
),
0.0
)
..
lineTo
(-(
_kToolbarTriangleSize
.
width
/
2
),
triangleBottomY
)
..
close
();
canvas
.
drawPath
(
triangle
,
paint
);
}
...
...
@@ -68,12 +84,14 @@ class _TextSelectionToolbar extends StatelessWidget {
this
.
handleCopy
,
this
.
handlePaste
,
this
.
handleSelectAll
,
this
.
arrowDirection
,
})
:
super
(
key:
key
);
final
VoidCallback
handleCut
;
final
VoidCallback
handleCopy
;
final
VoidCallback
handlePaste
;
final
VoidCallback
handleSelectAll
;
final
_ArrowDirection
arrowDirection
;
@override
Widget
build
(
BuildContext
context
)
{
...
...
@@ -103,35 +121,47 @@ class _TextSelectionToolbar extends StatelessWidget {
items
.
add
(
_buildToolbarButton
(
localizations
.
selectAllButtonLabel
,
handleSelectAll
));
}
const
Widget
padding
=
Padding
(
padding:
EdgeInsets
.
only
(
bottom:
10.0
));
final
Widget
triangle
=
SizedBox
.
fromSize
(
size:
_kToolbarTriangleSize
,
child:
CustomPaint
(
painter:
_TextSelectionToolbarNotchPainter
(),
painter:
_TextSelectionToolbarNotchPainter
(
arrowDirection
),
),
);
return
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
<
Widget
>[
ClipRRect
(
final
Widget
toolbar
=
ClipRRect
(
borderRadius:
_kToolbarBorderRadius
,
child:
DecoratedBox
(
decoration:
BoxDecoration
(
color:
_kToolbarDividerColor
,
borderRadius:
_kToolbarBorderRadius
,
child:
DecoratedBox
(
decoration:
BoxDecoration
(
color:
_kToolbarDividerColor
,
borderRadius:
_kToolbarBorderRadius
,
// Add a hairline border with the button color to avoid
// antialiasing artifacts.
border:
Border
.
all
(
color:
_kToolbarBackgroundColor
,
width:
0
),
),
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
items
),
),
// Add a hairline border with the button color to avoid
// antialiasing artifacts.
border:
Border
.
all
(
color:
_kToolbarBackgroundColor
,
width:
0
),
),
// TODO(xster): Position the triangle based on the layout delegate, and
// avoid letting the triangle line up with any dividers.
// https://github.com/flutter/flutter/issues/11274
triangle
,
const
Padding
(
padding:
EdgeInsets
.
only
(
bottom:
10.0
)),
],
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
items
),
),
);
final
List
<
Widget
>
menus
=
(
arrowDirection
==
_ArrowDirection
.
down
)
?
<
Widget
>[
toolbar
,
// TODO(xster): Position the triangle based on the layout delegate, and
// avoid letting the triangle line up with any dividers.
// https://github.com/flutter/flutter/issues/11274
triangle
,
padding
,
]
:
<
Widget
>[
padding
,
triangle
,
toolbar
,
];
return
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
menus
,
);
}
...
...
@@ -236,21 +266,49 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
/// Builder for iOS-style copy/paste text selection toolbar.
@override
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
Offset
position
,
TextSelectionDelegate
delegate
)
{
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
)
{
assert
(
debugCheckHasMediaQuery
(
context
));
// The toolbar should appear below the TextField
// when there is not enough space above the TextField to show it.
final
double
availableHeight
=
globalEditableRegion
.
top
-
MediaQuery
.
of
(
context
).
padding
.
top
-
_kToolbarScreenPadding
;
final
_ArrowDirection
direction
=
(
availableHeight
>
_kToolbarHeight
)
?
_ArrowDirection
.
down
:
_ArrowDirection
.
up
;
final
TextSelectionPoint
startTextSelectionPoint
=
endpoints
[
0
];
final
TextSelectionPoint
endTextSelectionPoint
=
(
endpoints
.
length
>
1
)
?
endpoints
[
1
]
:
null
;
final
double
x
=
(
endTextSelectionPoint
==
null
)
?
startTextSelectionPoint
.
point
.
dx
:
(
startTextSelectionPoint
.
point
.
dx
+
endTextSelectionPoint
.
point
.
dx
)
/
2.0
;
final
double
y
=
(
direction
==
_ArrowDirection
.
up
)
?
startTextSelectionPoint
.
point
.
dy
+
globalEditableRegion
.
height
+
_kToolbarHeight
:
startTextSelectionPoint
.
point
.
dy
-
globalEditableRegion
.
height
;
final
Offset
preciseMidpoint
=
Offset
(
x
,
y
);
return
ConstrainedBox
(
constraints:
BoxConstraints
.
tight
(
globalEditableRegion
.
size
),
child:
CustomSingleChildLayout
(
delegate:
_TextSelectionToolbarLayout
(
MediaQuery
.
of
(
context
).
size
,
globalEditableRegion
,
p
osition
,
p
reciseMidpoint
,
),
child:
_TextSelectionToolbar
(
handleCut:
canCut
(
delegate
)
?
()
=>
handleCut
(
delegate
)
:
null
,
handleCopy:
canCopy
(
delegate
)
?
()
=>
handleCopy
(
delegate
)
:
null
,
handlePaste:
canPaste
(
delegate
)
?
()
=>
handlePaste
(
delegate
)
:
null
,
handleSelectAll:
canSelectAll
(
delegate
)
?
()
=>
handleSelectAll
(
delegate
)
:
null
,
arrowDirection:
direction
,
),
),
);
...
...
packages/flutter/lib/src/material/text_selection.dart
View file @
57d66664
...
...
@@ -14,9 +14,11 @@ import 'material_localizations.dart';
import
'theme.dart'
;
const
double
_kHandleSize
=
22.0
;
// Minimal padding from all edges of the selection toolbar to all edges of the
// viewport.
const
double
_kToolbarScreenPadding
=
8.0
;
const
double
_kToolbarHeight
=
44.0
;
/// Manages a copy/paste text selection toolbar.
class
_TextSelectionToolbar
extends
StatelessWidget
{
...
...
@@ -50,7 +52,7 @@ class _TextSelectionToolbar extends StatelessWidget {
return
Material
(
elevation:
1.0
,
child:
Container
(
height:
44.0
,
height:
_kToolbarHeight
,
child:
Row
(
mainAxisSize:
MainAxisSize
.
min
,
children:
items
),
),
);
...
...
@@ -130,16 +132,39 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
/// Builder for material-style copy/paste text selection toolbar.
@override
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
Offset
position
,
TextSelectionDelegate
delegate
)
{
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
)
{
assert
(
debugCheckHasMediaQuery
(
context
));
assert
(
debugCheckHasMaterialLocalizations
(
context
));
// The toolbar should appear below the TextField
// when there is not enough space above the TextField to show it.
final
TextSelectionPoint
startTextSelectionPoint
=
endpoints
[
0
];
final
TextSelectionPoint
endTextSelectionPoint
=
(
endpoints
.
length
>
1
)
?
endpoints
[
1
]
:
null
;
final
double
x
=
(
endTextSelectionPoint
==
null
)
?
startTextSelectionPoint
.
point
.
dx
:
(
startTextSelectionPoint
.
point
.
dx
+
endTextSelectionPoint
.
point
.
dx
)
/
2.0
;
final
double
availableHeight
=
globalEditableRegion
.
top
-
MediaQuery
.
of
(
context
).
padding
.
top
-
_kToolbarScreenPadding
;
final
double
y
=
(
availableHeight
<
_kToolbarHeight
)
?
startTextSelectionPoint
.
point
.
dy
+
globalEditableRegion
.
height
+
_kToolbarHeight
+
_kToolbarScreenPadding
:
startTextSelectionPoint
.
point
.
dy
-
globalEditableRegion
.
height
;
final
Offset
preciseMidpoint
=
Offset
(
x
,
y
);
return
ConstrainedBox
(
constraints:
BoxConstraints
.
tight
(
globalEditableRegion
.
size
),
child:
CustomSingleChildLayout
(
delegate:
_TextSelectionToolbarLayout
(
MediaQuery
.
of
(
context
).
size
,
globalEditableRegion
,
p
osition
,
p
reciseMidpoint
,
),
child:
_TextSelectionToolbar
(
handleCut:
canCut
(
delegate
)
?
()
=>
handleCut
(
delegate
)
:
null
,
...
...
packages/flutter/lib/src/widgets/text_selection.dart
View file @
57d66664
...
...
@@ -97,7 +97,20 @@ abstract class TextSelectionControls {
/// Builds a toolbar near a text selection.
///
/// Typically displays buttons for copying and pasting text.
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
Offset
position
,
TextSelectionDelegate
delegate
);
///
/// [globalEditableRegion] is the TextField size of the global coordinate system
/// in logical pixels.
///
/// The [position] is a general calculation midpoint parameter of the toolbar.
/// If you want more detailed position information, can use [endpoints]
/// to calculate it.
Widget
buildToolbar
(
BuildContext
context
,
Rect
globalEditableRegion
,
Offset
position
,
List
<
TextSelectionPoint
>
endpoints
,
TextSelectionDelegate
delegate
,
);
/// Returns the size of the selection handle.
Size
get
handleSize
;
...
...
@@ -439,7 +452,13 @@ class TextSelectionOverlay {
link:
layerLink
,
showWhenUnlinked:
false
,
offset:
-
editingRegion
.
topLeft
,
child:
selectionControls
.
buildToolbar
(
context
,
editingRegion
,
midpoint
,
selectionDelegate
),
child:
selectionControls
.
buildToolbar
(
context
,
editingRegion
,
midpoint
,
endpoints
,
selectionDelegate
,
),
),
);
}
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
57d66664
...
...
@@ -2042,6 +2042,76 @@ void main() {
},
);
testWidgets
(
'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it'
,
(
WidgetTester
tester
)
async
{
// This is a regression test for
// https://github.com/flutter/flutter/issues/29808
const
String
testValue
=
'abc def ghi'
;
final
TextEditingController
controller
=
TextEditingController
();
await
tester
.
pumpWidget
(
CupertinoApp
(
home:
Container
(
padding:
const
EdgeInsets
.
all
(
30
),
child:
CupertinoTextField
(
controller:
controller
,
),
),
),
);
await
tester
.
enterText
(
find
.
byType
(
CupertinoTextField
),
testValue
);
// Tap the selection handle to bring up the "paste / select all" menu.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
controller
.
selection
),
renderEditable
,
);
await
tester
.
tapAt
(
endpoints
[
0
].
point
+
const
Offset
(
1.0
,
1.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
// Verify the selection toolbar position
Offset
toolbarTopLeft
=
tester
.
getTopLeft
(
find
.
text
(
'Paste'
));
Offset
textFieldTopLeft
=
tester
.
getTopLeft
(
find
.
byType
(
CupertinoTextField
));
expect
(
textFieldTopLeft
.
dy
,
lessThan
(
toolbarTopLeft
.
dy
));
await
tester
.
pumpWidget
(
CupertinoApp
(
home:
Container
(
padding:
const
EdgeInsets
.
all
(
150
),
child:
CupertinoTextField
(
controller:
controller
,
),
),
),
);
await
tester
.
enterText
(
find
.
byType
(
CupertinoTextField
),
testValue
);
// Tap the selection handle to bring up the "paste / select all" menu.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
renderEditable
=
findRenderEditable
(
tester
);
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
controller
.
selection
),
renderEditable
,
);
await
tester
.
tapAt
(
endpoints
[
0
].
point
+
const
Offset
(
1.0
,
1.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
// Verify the selection toolbar position
toolbarTopLeft
=
tester
.
getTopLeft
(
find
.
text
(
'Paste'
));
textFieldTopLeft
=
tester
.
getTopLeft
(
find
.
byType
(
CupertinoTextField
));
expect
(
toolbarTopLeft
.
dy
,
lessThan
(
textFieldTopLeft
.
dy
));
}
);
testWidgets
(
'text field respects keyboardAppearance from theme'
,
(
WidgetTester
tester
)
async
{
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
SystemChannels
.
textInput
.
setMockMethodCallHandler
((
MethodCall
methodCall
)
async
{
...
...
packages/flutter/test/material/text_field_test.dart
View file @
57d66664
...
...
@@ -1126,6 +1126,82 @@ void main() {
expect
(
controller
.
text
,
'abc d
${testValue}
ef ghi'
);
});
testWidgets
(
'Check the toolbar appears below the TextField when there is not enough space above the TextField to show it'
,
(
WidgetTester
tester
)
async
{
// This is a regression test for
// https://github.com/flutter/flutter/issues/29808
final
TextEditingController
controller
=
TextEditingController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Padding
(
padding:
const
EdgeInsets
.
all
(
30.0
),
child:
TextField
(
controller:
controller
,
),
),
),
),
);
const
String
testValue
=
'abc def ghi'
;
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
await
skipPastScrollingAnimation
(
tester
);
// Tap the selection handle to bring up the "paste / select all" menu.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
RenderEditable
renderEditable
=
findRenderEditable
(
tester
);
List
<
TextSelectionPoint
>
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
controller
.
selection
),
renderEditable
,
);
await
tester
.
tapAt
(
endpoints
[
0
].
point
+
const
Offset
(
1.0
,
1.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
// Verify the selection toolbar position
Offset
toolbarTopLeft
=
tester
.
getTopLeft
(
find
.
text
(
'SELECT ALL'
));
Offset
textFieldTopLeft
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
expect
(
textFieldTopLeft
.
dy
,
lessThan
(
toolbarTopLeft
.
dy
));
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Padding
(
padding:
const
EdgeInsets
.
all
(
150.0
),
child:
TextField
(
controller:
controller
,
),
),
),
),
);
await
tester
.
enterText
(
find
.
byType
(
TextField
),
testValue
);
await
skipPastScrollingAnimation
(
tester
);
// Tap the selection handle to bring up the "paste / select all" menu.
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'e'
)));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
renderEditable
=
findRenderEditable
(
tester
);
endpoints
=
globalize
(
renderEditable
.
getEndpointsForSelection
(
controller
.
selection
),
renderEditable
,
);
await
tester
.
tapAt
(
endpoints
[
0
].
point
+
const
Offset
(
1.0
,
1.0
));
await
tester
.
pump
();
await
tester
.
pump
(
const
Duration
(
milliseconds:
200
));
// skip past the frame where the opacity is zero
// Verify the selection toolbar position
toolbarTopLeft
=
tester
.
getTopLeft
(
find
.
text
(
'SELECT ALL'
));
textFieldTopLeft
=
tester
.
getTopLeft
(
find
.
byType
(
TextField
));
expect
(
toolbarTopLeft
.
dy
,
lessThan
(
textFieldTopLeft
.
dy
));
}
);
testWidgets
(
'Selection toolbar fades in'
,
(
WidgetTester
tester
)
async
{
final
TextEditingController
controller
=
TextEditingController
();
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
57d66664
...
...
@@ -1588,7 +1588,7 @@ void main() {
controls
=
MockTextSelectionControls
();
when
(
controls
.
buildHandle
(
any
,
any
,
any
)).
thenReturn
(
Container
());
when
(
controls
.
buildToolbar
(
any
,
any
,
any
,
any
))
when
(
controls
.
buildToolbar
(
any
,
any
,
any
,
any
,
any
))
.
thenReturn
(
Container
());
});
...
...
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