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
527fddc6
Commit
527fddc6
authored
Feb 16, 2017
by
Adam Barth
Committed by
GitHub
Feb 16, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Port EditableText to Scrollable2 (#8167)
parent
3985ddbc
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
169 additions
and
163 deletions
+169
-163
rendering.dart
packages/flutter/lib/rendering.dart
+1
-1
input.dart
packages/flutter/lib/src/material/input.dart
+0
-1
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+106
-58
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+57
-98
single_child_scroll_view.dart
...ges/flutter/lib/src/widgets/single_child_scroll_view.dart
+4
-4
input_test.dart
packages/flutter/test/widgets/input_test.dart
+1
-1
No files found.
packages/flutter/lib/rendering.dart
View file @
527fddc6
...
@@ -28,7 +28,7 @@ export 'src/rendering/block.dart';
...
@@ -28,7 +28,7 @@ export 'src/rendering/block.dart';
export
'src/rendering/box.dart'
;
export
'src/rendering/box.dart'
;
export
'src/rendering/custom_layout.dart'
;
export
'src/rendering/custom_layout.dart'
;
export
'src/rendering/debug.dart'
;
export
'src/rendering/debug.dart'
;
export
'src/rendering/editable
_line
.dart'
;
export
'src/rendering/editable.dart'
;
export
'src/rendering/error.dart'
;
export
'src/rendering/error.dart'
;
export
'src/rendering/flex.dart'
;
export
'src/rendering/flex.dart'
;
export
'src/rendering/flow.dart'
;
export
'src/rendering/flow.dart'
;
...
...
packages/flutter/lib/src/material/input.dart
View file @
527fddc6
...
@@ -147,7 +147,6 @@ class _InputFieldState extends State<InputField> {
...
@@ -147,7 +147,6 @@ class _InputFieldState extends State<InputField> {
cursorColor:
themeData
.
textSelectionColor
,
cursorColor:
themeData
.
textSelectionColor
,
selectionColor:
themeData
.
textSelectionColor
,
selectionColor:
themeData
.
textSelectionColor
,
selectionControls:
materialTextSelectionControls
,
selectionControls:
materialTextSelectionControls
,
platform:
Theme
.
of
(
context
).
platform
,
keyboardType:
config
.
keyboardType
,
keyboardType:
config
.
keyboardType
,
onChanged:
config
.
onChanged
,
onChanged:
config
.
onChanged
,
onSubmitted:
config
.
onSubmitted
,
onSubmitted:
config
.
onSubmitted
,
...
...
packages/flutter/lib/src/rendering/editable
_line
.dart
→
packages/flutter/lib/src/rendering/editable.dart
View file @
527fddc6
...
@@ -2,13 +2,15 @@
...
@@ -2,13 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
as
ui
show
TextBox
;
import
'dart:ui'
as
ui
show
TextBox
;
import
'package:flutter/gestures.dart'
;
import
'package:flutter/gestures.dart'
;
import
'package:meta/meta.dart'
;
import
'box.dart'
;
import
'box.dart'
;
import
'object.dart'
;
import
'object.dart'
;
import
'viewport.dart'
;
import
'viewport
_offset
.dart'
;
const
double
_kCaretGap
=
1.0
;
// pixels
const
double
_kCaretGap
=
1.0
;
// pixels
const
double
_kCaretHeightOffset
=
2.0
;
// pixels
const
double
_kCaretHeightOffset
=
2.0
;
// pixels
...
@@ -39,14 +41,6 @@ class TextSelectionPoint {
...
@@ -39,14 +41,6 @@ class TextSelectionPoint {
final
TextDirection
direction
;
final
TextDirection
direction
;
}
}
/// Signature for the callback used by [RenderEditable] to determine the paint offset when
/// the dimensions of the render box change.
///
/// The return value should be the new paint offset to use.
///
/// Used by [RenderEditable.onPaintOffsetUpdateNeeded].
typedef
Offset
RenderEditablePaintOffsetNeededCallback
(
ViewportDimensions
dimensions
,
Rect
caretRect
);
/// A single line of editable text.
/// A single line of editable text.
class
RenderEditable
extends
RenderBox
{
class
RenderEditable
extends
RenderBox
{
/// Creates a render object for a single line of editable text.
/// Creates a render object for a single line of editable text.
...
@@ -58,15 +52,18 @@ class RenderEditable extends RenderBox {
...
@@ -58,15 +52,18 @@ class RenderEditable extends RenderBox {
Color
selectionColor
,
Color
selectionColor
,
double
textScaleFactor:
1.0
,
double
textScaleFactor:
1.0
,
TextSelection
selection
,
TextSelection
selection
,
@required
ViewportOffset
offset
,
this
.
onSelectionChanged
,
this
.
onSelectionChanged
,
Offset
paintOffset:
Offset
.
zero
,
this
.
onPaintOffsetUpdateNeeded
,
})
:
_textPainter
=
new
TextPainter
(
text:
text
,
textScaleFactor:
textScaleFactor
),
})
:
_textPainter
=
new
TextPainter
(
text:
text
,
textScaleFactor:
textScaleFactor
),
_cursorColor
=
cursorColor
,
_cursorColor
=
cursorColor
,
_showCursor
=
showCursor
,
_showCursor
=
showCursor
,
_maxLines
=
maxLines
,
_maxLines
=
maxLines
,
_selection
=
selection
,
_selection
=
selection
,
_paintOffset
=
paintOffset
{
_offset
=
offset
{
assert
(
showCursor
!=
null
);
assert
(
maxLines
!=
null
);
assert
(
textScaleFactor
!=
null
);
assert
(
offset
!=
null
);
assert
(!
showCursor
||
cursorColor
!=
null
);
assert
(!
showCursor
||
cursorColor
!=
null
);
_tap
=
new
TapGestureRecognizer
()
_tap
=
new
TapGestureRecognizer
()
..
onTapDown
=
_handleTapDown
..
onTapDown
=
_handleTapDown
...
@@ -79,9 +76,6 @@ class RenderEditable extends RenderBox {
...
@@ -79,9 +76,6 @@ class RenderEditable extends RenderBox {
/// Called when the selection changes.
/// Called when the selection changes.
SelectionChangedHandler
onSelectionChanged
;
SelectionChangedHandler
onSelectionChanged
;
/// Called when the inner or outer dimensions of this render object change.
RenderEditablePaintOffsetNeededCallback
onPaintOffsetUpdateNeeded
;
/// The text to display
/// The text to display
TextSpan
get
text
=>
_textPainter
.
text
;
TextSpan
get
text
=>
_textPainter
.
text
;
final
TextPainter
_textPainter
;
final
TextPainter
_textPainter
;
...
@@ -106,6 +100,7 @@ class RenderEditable extends RenderBox {
...
@@ -106,6 +100,7 @@ class RenderEditable extends RenderBox {
bool
get
showCursor
=>
_showCursor
;
bool
get
showCursor
=>
_showCursor
;
bool
_showCursor
;
bool
_showCursor
;
set
showCursor
(
bool
value
)
{
set
showCursor
(
bool
value
)
{
assert
(
value
!=
null
);
if
(
_showCursor
==
value
)
if
(
_showCursor
==
value
)
return
;
return
;
_showCursor
=
value
;
_showCursor
=
value
;
...
@@ -118,6 +113,7 @@ class RenderEditable extends RenderBox {
...
@@ -118,6 +113,7 @@ class RenderEditable extends RenderBox {
int
get
maxLines
=>
_maxLines
;
int
get
maxLines
=>
_maxLines
;
int
_maxLines
;
int
_maxLines
;
set
maxLines
(
int
value
)
{
set
maxLines
(
int
value
)
{
assert
(
value
!=
null
);
if
(
_maxLines
==
value
)
if
(
_maxLines
==
value
)
return
;
return
;
_maxLines
=
value
;
_maxLines
=
value
;
...
@@ -165,15 +161,58 @@ class RenderEditable extends RenderBox {
...
@@ -165,15 +161,58 @@ class RenderEditable extends RenderBox {
/// If the text content is larger than the editable line itself, the editable
/// If the text content is larger than the editable line itself, the editable
/// line clips the text. This property controls which part of the text is
/// line clips the text. This property controls which part of the text is
/// visible by shifting the text by the given offset before clipping.
/// visible by shifting the text by the given offset before clipping.
Offset
get
paintOffset
=>
_paintOffset
;
ViewportOffset
get
offset
=>
_offset
;
Offset
_paintOffset
;
ViewportOffset
_offset
;
set
paintOffset
(
Offset
value
)
{
set
offset
(
ViewportOffset
value
)
{
if
(
_paintOffset
==
value
)
assert
(
value
!=
null
);
if
(
_offset
==
value
)
return
;
return
;
_paintOffset
=
value
;
if
(
attached
)
markNeedsPaint
();
_offset
.
removeListener
(
markNeedsPaint
);
_offset
=
value
;
if
(
attached
)
_offset
.
addListener
(
markNeedsPaint
);
markNeedsLayout
();
}
bool
get
_isMultiline
=>
maxLines
>
1
;
Axis
get
_viewportAxis
=>
_isMultiline
?
Axis
.
vertical
:
Axis
.
horizontal
;
Offset
get
_paintOffset
{
switch
(
_viewportAxis
)
{
case
Axis
.
horizontal
:
return
new
Offset
(-
offset
.
pixels
,
0.0
);
case
Axis
.
vertical
:
return
new
Offset
(
0.0
,
-
offset
.
pixels
);
}
return
null
;
}
}
double
get
_viewportExtent
{
assert
(
hasSize
);
switch
(
_viewportAxis
)
{
case
Axis
.
horizontal
:
return
size
.
width
;
case
Axis
.
vertical
:
return
size
.
height
;
}
return
null
;
}
double
_getMaxScrollExtent
(
Size
contentSize
)
{
assert
(
hasSize
);
switch
(
_viewportAxis
)
{
case
Axis
.
horizontal
:
return
math
.
max
(
0.0
,
contentSize
.
width
-
size
.
width
);
case
Axis
.
vertical
:
return
math
.
max
(
0.0
,
contentSize
.
height
-
size
.
height
);
}
return
null
;
}
bool
_hasVisualOverflow
=
false
;
/// Returns the global coordinates of the endpoints of the given selection.
/// Returns the global coordinates of the endpoints of the given selection.
///
///
/// If the selection is collapsed (and therefore occupies a single point), the
/// If the selection is collapsed (and therefore occupies a single point), the
...
@@ -187,17 +226,17 @@ class RenderEditable extends RenderBox {
...
@@ -187,17 +226,17 @@ class RenderEditable extends RenderBox {
// clean at this point.
// clean at this point.
_textPainter
.
layout
(
maxWidth:
_maxContentWidth
);
_textPainter
.
layout
(
maxWidth:
_maxContentWidth
);
Offset
o
ffset
=
_paintOffset
;
final
Offset
paintO
ffset
=
_paintOffset
;
if
(
selection
.
isCollapsed
)
{
if
(
selection
.
isCollapsed
)
{
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
// TODO(mpcomplete): This doesn't work well at an RTL/LTR boundary.
Offset
caretOffset
=
_textPainter
.
getOffsetForCaret
(
selection
.
extent
,
_caretPrototype
);
final
Offset
caretOffset
=
_textPainter
.
getOffsetForCaret
(
selection
.
extent
,
_caretPrototype
);
Point
start
=
new
Point
(
0.0
,
_preferredLineHeight
)
+
caretOffset
+
o
ffset
;
final
Point
start
=
new
Point
(
0.0
,
_preferredLineHeight
)
+
caretOffset
+
paintO
ffset
;
return
<
TextSelectionPoint
>[
new
TextSelectionPoint
(
localToGlobal
(
start
),
null
)];
return
<
TextSelectionPoint
>[
new
TextSelectionPoint
(
localToGlobal
(
start
),
null
)];
}
else
{
}
else
{
List
<
ui
.
TextBox
>
boxes
=
_textPainter
.
getBoxesForSelection
(
selection
);
final
List
<
ui
.
TextBox
>
boxes
=
_textPainter
.
getBoxesForSelection
(
selection
);
Point
start
=
new
Point
(
boxes
.
first
.
start
,
boxes
.
first
.
bottom
)
+
o
ffset
;
final
Point
start
=
new
Point
(
boxes
.
first
.
start
,
boxes
.
first
.
bottom
)
+
paintO
ffset
;
Point
end
=
new
Point
(
boxes
.
last
.
end
,
boxes
.
last
.
bottom
)
+
o
ffset
;
final
Point
end
=
new
Point
(
boxes
.
last
.
end
,
boxes
.
last
.
bottom
)
+
paintO
ffset
;
return
<
TextSelectionPoint
>[
return
<
TextSelectionPoint
>[
new
TextSelectionPoint
(
localToGlobal
(
start
),
boxes
.
first
.
direction
),
new
TextSelectionPoint
(
localToGlobal
(
start
),
boxes
.
first
.
direction
),
new
TextSelectionPoint
(
localToGlobal
(
end
),
boxes
.
last
.
direction
),
new
TextSelectionPoint
(
localToGlobal
(
end
),
boxes
.
last
.
direction
),
...
@@ -207,26 +246,24 @@ class RenderEditable extends RenderBox {
...
@@ -207,26 +246,24 @@ class RenderEditable extends RenderBox {
/// Returns the position in the text for the given global coordinate.
/// Returns the position in the text for the given global coordinate.
TextPosition
getPositionForPoint
(
Point
globalPosition
)
{
TextPosition
getPositionForPoint
(
Point
globalPosition
)
{
globalPosition
+=
-
paintOffset
;
globalPosition
+=
-
_
paintOffset
;
return
_textPainter
.
getPositionForOffset
(
globalToLocal
(
globalPosition
).
toOffset
());
return
_textPainter
.
getPositionForOffset
(
globalToLocal
(
globalPosition
).
toOffset
());
}
}
/// Returns the Rect in local coordinates for the caret at the given text
/// Returns the Rect in local coordinates for the caret at the given text
/// position.
/// position.
Rect
getLocalRectForCaret
(
TextPosition
caretPosition
)
{
Rect
getLocalRectForCaret
(
TextPosition
caretPosition
)
{
Offset
caretOffset
=
_textPainter
.
getOffsetForCaret
(
caretPosition
,
_caretPrototype
);
final
Offset
caretOffset
=
_textPainter
.
getOffsetForCaret
(
caretPosition
,
_caretPrototype
);
// This rect is the same as _caretPrototype but without the vertical padding.
// This rect is the same as _caretPrototype but without the vertical padding.
return
new
Rect
.
fromLTWH
(
0.0
,
0.0
,
_kCaretWidth
,
_preferredLineHeight
).
shift
(
caretOffset
+
_paintOffset
);
return
new
Rect
.
fromLTWH
(
0.0
,
0.0
,
_kCaretWidth
,
_preferredLineHeight
).
shift
(
caretOffset
+
_paintOffset
);
}
}
Size
_contentSize
;
double
get
_preferredLineHeight
=>
_textPainter
.
preferredLineHeight
;
double
get
_preferredLineHeight
=>
_textPainter
.
preferredLineHeight
;
double
get
_maxContentWidth
{
double
get
_maxContentWidth
{
return
_maxLines
>
1
?
if
(
_maxLines
>
1
)
constraints
.
maxWidth
-
(
_kCaretGap
+
_kCaretWidth
)
:
return
constraints
.
maxWidth
-
(
_kCaretGap
+
_kCaretWidth
);
double
.
INFINITY
;
return
double
.
INFINITY
;
}
}
@override
@override
...
@@ -257,15 +294,15 @@ class RenderEditable extends RenderBox {
...
@@ -257,15 +294,15 @@ class RenderEditable extends RenderBox {
Point
_lastTapDownPosition
;
Point
_lastTapDownPosition
;
Point
_longPressPosition
;
Point
_longPressPosition
;
void
_handleTapDown
(
TapDownDetails
details
)
{
void
_handleTapDown
(
TapDownDetails
details
)
{
_lastTapDownPosition
=
details
.
globalPosition
+
-
paintOffset
;
_lastTapDownPosition
=
details
.
globalPosition
+
-
_
paintOffset
;
}
}
void
_handleTap
()
{
void
_handleTap
()
{
assert
(
_lastTapDownPosition
!=
null
);
assert
(
_lastTapDownPosition
!=
null
);
final
Point
global
=
_lastTapDownPosition
;
final
Point
global
Position
=
_lastTapDownPosition
;
_lastTapDownPosition
=
null
;
_lastTapDownPosition
=
null
;
if
(
onSelectionChanged
!=
null
)
{
if
(
onSelectionChanged
!=
null
)
{
TextPosition
position
=
_textPainter
.
getPositionForOffset
(
globalToLocal
(
global
).
toOffset
());
TextPosition
position
=
_textPainter
.
getPositionForOffset
(
globalToLocal
(
global
Position
).
toOffset
());
onSelectionChanged
(
new
TextSelection
.
fromPosition
(
position
),
this
,
false
);
onSelectionChanged
(
new
TextSelection
.
fromPosition
(
position
),
this
,
false
);
}
}
}
}
...
@@ -277,16 +314,16 @@ class RenderEditable extends RenderBox {
...
@@ -277,16 +314,16 @@ class RenderEditable extends RenderBox {
}
}
void
_handleLongPress
()
{
void
_handleLongPress
()
{
final
Point
global
=
_longPressPosition
;
final
Point
global
Position
=
_longPressPosition
;
_longPressPosition
=
null
;
_longPressPosition
=
null
;
if
(
onSelectionChanged
!=
null
)
{
if
(
onSelectionChanged
!=
null
)
{
TextPosition
position
=
_textPainter
.
getPositionForOffset
(
globalToLocal
(
global
).
toOffset
());
final
TextPosition
position
=
_textPainter
.
getPositionForOffset
(
globalToLocal
(
globalPosition
).
toOffset
());
onSelectionChanged
(
_selectWordAtOffset
(
position
),
this
,
true
);
onSelectionChanged
(
_selectWordAtOffset
(
position
),
this
,
true
);
}
}
}
}
TextSelection
_selectWordAtOffset
(
TextPosition
position
)
{
TextSelection
_selectWordAtOffset
(
TextPosition
position
)
{
TextRange
word
=
_textPainter
.
getWordBoundary
(
position
);
final
TextRange
word
=
_textPainter
.
getWordBoundary
(
position
);
// When long-pressing past the end of the text, we want a collapsed cursor.
// When long-pressing past the end of the text, we want a collapsed cursor.
if
(
position
.
offset
>=
word
.
end
)
if
(
position
.
offset
>=
word
.
end
)
return
new
TextSelection
.
fromPosition
(
position
);
return
new
TextSelection
.
fromPosition
(
position
);
...
@@ -297,41 +334,34 @@ class RenderEditable extends RenderBox {
...
@@ -297,41 +334,34 @@ class RenderEditable extends RenderBox {
@override
@override
void
performLayout
()
{
void
performLayout
()
{
Size
oldSize
=
hasSize
?
size
:
null
;
_caretPrototype
=
new
Rect
.
fromLTWH
(
0.0
,
_kCaretHeightOffset
,
_kCaretWidth
,
_preferredLineHeight
-
2.0
*
_kCaretHeightOffset
);
_caretPrototype
=
new
Rect
.
fromLTWH
(
0.0
,
_kCaretHeightOffset
,
_kCaretWidth
,
_preferredLineHeight
-
2.0
*
_kCaretHeightOffset
);
_selectionRects
=
null
;
_selectionRects
=
null
;
_textPainter
.
layout
(
maxWidth:
_maxContentWidth
);
_textPainter
.
layout
(
maxWidth:
_maxContentWidth
);
size
=
new
Size
(
constraints
.
maxWidth
,
constraints
.
constrainHeight
(
size
=
new
Size
(
constraints
.
maxWidth
,
constraints
.
constrainHeight
(
_textPainter
.
height
.
clamp
(
_preferredLineHeight
,
_preferredLineHeight
*
_maxLines
)
_textPainter
.
height
.
clamp
(
_preferredLineHeight
,
_preferredLineHeight
*
_maxLines
)
));
));
Size
contentSize
=
new
Size
(
_textPainter
.
width
+
_kCaretGap
+
_kCaretWidth
,
_textPainter
.
height
);
final
Size
contentSize
=
new
Size
(
_textPainter
.
width
+
_kCaretGap
+
_kCaretWidth
,
_textPainter
.
height
);
assert
(
_selection
!=
null
);
final
double
_maxScrollExtent
=
_getMaxScrollExtent
(
contentSize
);
Rect
caretRect
=
getLocalRectForCaret
(
_selection
.
extent
);
_hasVisualOverflow
=
_maxScrollExtent
>
0.0
;
if
(
onPaintOffsetUpdateNeeded
!=
null
&&
(
size
!=
oldSize
||
contentSize
!=
_contentSize
||
!
_withinBounds
(
caretRect
)))
offset
.
applyViewportDimension
(
_viewportExtent
);
onPaintOffsetUpdateNeeded
(
new
ViewportDimensions
(
containerSize:
size
,
contentSize:
contentSize
),
caretRect
);
offset
.
applyContentDimensions
(
0.0
,
_maxScrollExtent
);
_contentSize
=
contentSize
;
}
bool
_withinBounds
(
Rect
caretRect
)
{
Rect
bounds
=
new
Rect
.
fromLTWH
(
0.0
,
0.0
,
size
.
width
,
size
.
height
);
return
(
bounds
.
contains
(
caretRect
.
topLeft
)
&&
bounds
.
contains
(
caretRect
.
bottomRight
));
}
}
void
_paintCaret
(
Canvas
canvas
,
Offset
effectiveOffset
)
{
void
_paintCaret
(
Canvas
canvas
,
Offset
effectiveOffset
)
{
Offset
caretOffset
=
_textPainter
.
getOffsetForCaret
(
_selection
.
extent
,
_caretPrototype
);
final
Offset
caretOffset
=
_textPainter
.
getOffsetForCaret
(
_selection
.
extent
,
_caretPrototype
);
Paint
paint
=
new
Paint
()..
color
=
_cursorColor
;
final
Paint
paint
=
new
Paint
()..
color
=
_cursorColor
;
canvas
.
drawRect
(
_caretPrototype
.
shift
(
caretOffset
+
effectiveOffset
),
paint
);
canvas
.
drawRect
(
_caretPrototype
.
shift
(
caretOffset
+
effectiveOffset
),
paint
);
}
}
void
_paintSelection
(
Canvas
canvas
,
Offset
effectiveOffset
)
{
void
_paintSelection
(
Canvas
canvas
,
Offset
effectiveOffset
)
{
assert
(
_selectionRects
!=
null
);
assert
(
_selectionRects
!=
null
);
Paint
paint
=
new
Paint
()..
color
=
_selectionColor
;
final
Paint
paint
=
new
Paint
()..
color
=
_selectionColor
;
for
(
ui
.
TextBox
box
in
_selectionRects
)
for
(
ui
.
TextBox
box
in
_selectionRects
)
canvas
.
drawRect
(
box
.
toRect
().
shift
(
effectiveOffset
),
paint
);
canvas
.
drawRect
(
box
.
toRect
().
shift
(
effectiveOffset
),
paint
);
}
}
void
_paintContents
(
PaintingContext
context
,
Offset
offset
)
{
void
_paintContents
(
PaintingContext
context
,
Offset
offset
)
{
Offset
effectiveOffset
=
offset
+
_paintOffset
;
final
Offset
effectiveOffset
=
offset
+
_paintOffset
;
if
(
_selection
!=
null
)
{
if
(
_selection
!=
null
)
{
if
(
_selection
.
isCollapsed
&&
_showCursor
&&
cursorColor
!=
null
)
{
if
(
_selection
.
isCollapsed
&&
_showCursor
&&
cursorColor
!=
null
)
{
...
@@ -345,8 +375,6 @@ class RenderEditable extends RenderBox {
...
@@ -345,8 +375,6 @@ class RenderEditable extends RenderBox {
_textPainter
.
paint
(
context
.
canvas
,
effectiveOffset
);
_textPainter
.
paint
(
context
.
canvas
,
effectiveOffset
);
}
}
bool
get
_hasVisualOverflow
=>
_contentSize
.
width
>
size
.
width
;
@override
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
if
(
_hasVisualOverflow
)
if
(
_hasVisualOverflow
)
...
@@ -357,4 +385,24 @@ class RenderEditable extends RenderBox {
...
@@ -357,4 +385,24 @@ class RenderEditable extends RenderBox {
@override
@override
Rect
describeApproximatePaintClip
(
RenderObject
child
)
=>
_hasVisualOverflow
?
Point
.
origin
&
size
:
null
;
Rect
describeApproximatePaintClip
(
RenderObject
child
)
=>
_hasVisualOverflow
?
Point
.
origin
&
size
:
null
;
@override
void
debugFillDescription
(
List
<
String
>
description
)
{
super
.
debugFillDescription
(
description
);
description
.
add
(
'cursorColor:
$_cursorColor
'
);
description
.
add
(
'showCursor:
$_showCursor
'
);
description
.
add
(
'maxLines:
$_maxLines
'
);
description
.
add
(
'selectionColor:
$_selectionColor
'
);
description
.
add
(
'textScaleFactor:
$textScaleFactor
'
);
description
.
add
(
'selection:
$_selection
'
);
description
.
add
(
'offset:
$_offset
'
);
}
@override
String
debugDescribeChildren
(
String
prefix
)
{
return
'
$prefix
\
u2558
\
u2550
\
u2566
\
u2550
\
u2550 text
\
u2550
\
u2550
\
u2550
\n
'
'
${text.toString("$prefix \u2551 ")}
'
// TextSpan includes a newline
'
$prefix
\
u255A
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\
u2550
\n
'
'
$prefix
\n
'
;
}
}
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
527fddc6
...
@@ -5,14 +5,15 @@
...
@@ -5,14 +5,15 @@
import
'dart:async'
;
import
'dart:async'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/rendering.dart'
show
RenderEditable
,
SelectionChangedHandler
,
RenderEditablePaintOffsetNeededCallback
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/services.dart'
;
import
'package:flutter/services.dart'
;
import
'basic.dart'
;
import
'basic.dart'
;
import
'focus.dart'
;
import
'focus.dart'
;
import
'framework.dart'
;
import
'framework.dart'
;
import
'media_query.dart'
;
import
'media_query.dart'
;
import
'scroll_behavior.dart'
;
import
'scroll_controller.dart'
;
import
'scroll_physics.dart'
;
import
'scrollable.dart'
;
import
'scrollable.dart'
;
import
'text_selection.dart'
;
import
'text_selection.dart'
;
...
@@ -135,32 +136,33 @@ class InputValue {
...
@@ -135,32 +136,33 @@ class InputValue {
/// * [InputField], which adds tap-to-focus and cut, copy, and paste commands.
/// * [InputField], which adds tap-to-focus and cut, copy, and paste commands.
/// * [TextField], which is a full-featured, material-design text input field
/// * [TextField], which is a full-featured, material-design text input field
/// with placeholder text, labels, and [Form] integration.
/// with placeholder text, labels, and [Form] integration.
class
EditableText
extends
S
crollable
{
// ignore: DEPRECATED_MEMBER_USE
class
EditableText
extends
S
tatefulWidget
{
/// Creates a basic text input control.
/// Creates a basic text input control.
///
///
/// The [value] argument must not be null.
/// The [value] argument must not be null.
EditableText
({
EditableText
({
Key
key
,
Key
key
,
@required
this
.
value
,
@required
this
.
value
,
this
.
focusKey
,
@required
this
.
focusKey
,
this
.
obscureText
:
false
,
this
.
obscureText
:
false
,
this
.
style
,
@required
this
.
style
,
this
.
cursorColor
,
@required
this
.
cursorColor
,
this
.
textScaleFactor
,
this
.
textScaleFactor
,
int
maxLines:
1
,
this
.
maxLines
:
1
,
this
.
autofocus
:
false
,
this
.
autofocus
:
false
,
this
.
selectionColor
,
this
.
selectionColor
,
this
.
selectionControls
,
this
.
selectionControls
,
@required
this
.
platform
,
this
.
keyboardType
,
this
.
keyboardType
,
this
.
onChanged
,
this
.
onChanged
,
this
.
onSubmitted
this
.
onSubmitted
,
})
:
maxLines
=
maxLines
,
super
(
})
:
super
(
key:
key
)
{
key:
key
,
initialScrollOffset:
0.0
,
scrollDirection:
maxLines
>
1
?
Axis
.
vertical
:
Axis
.
horizontal
)
{
assert
(
value
!=
null
);
assert
(
value
!=
null
);
assert
(
focusKey
!=
null
);
assert
(
obscureText
!=
null
);
assert
(
style
!=
null
);
assert
(
cursorColor
!=
null
);
assert
(
maxLines
!=
null
);
assert
(
autofocus
!=
null
);
}
}
/// The string being displayed in this widget.
/// The string being displayed in this widget.
...
@@ -206,12 +208,6 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE
...
@@ -206,12 +208,6 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE
/// Optional delegate for building the text selection handles and toolbar.
/// Optional delegate for building the text selection handles and toolbar.
final
TextSelectionControls
selectionControls
;
final
TextSelectionControls
selectionControls
;
/// The platform whose behavior should be approximated, in particular
/// for scroll physics. (See [ScrollBehavior.platform].)
///
/// Must not be null.
final
TargetPlatform
platform
;
/// The type of keyboard to use for editing the text.
/// The type of keyboard to use for editing the text.
final
TextInputType
keyboardType
;
final
TextInputType
keyboardType
;
...
@@ -226,7 +222,7 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE
...
@@ -226,7 +222,7 @@ class EditableText extends Scrollable { // ignore: DEPRECATED_MEMBER_USE
}
}
/// State for a [EditableText].
/// State for a [EditableText].
class
EditableTextState
extends
S
crollableState
<
EditableText
>
implements
TextInputClient
{
// ignore: DEPRECATED_MEMBER_USE
class
EditableTextState
extends
S
tate
<
EditableText
>
implements
TextInputClient
{
Timer
_cursorTimer
;
Timer
_cursorTimer
;
bool
_showCursor
=
false
;
bool
_showCursor
=
false
;
...
@@ -234,11 +230,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -234,11 +230,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
TextInputConnection
_textInputConnection
;
TextInputConnection
_textInputConnection
;
TextSelectionOverlay
_selectionOverlay
;
TextSelectionOverlay
_selectionOverlay
;
@override
final
ScrollController
_scrollController
=
new
ScrollController
();
ExtentScrollBehavior
createScrollBehavior
()
=>
new
BoundedBehavior
(
platform:
config
.
platform
);
@override
BoundedBehavior
get
scrollBehavior
=>
super
.
scrollBehavior
;
@override
@override
void
initState
()
{
void
initState
()
{
...
@@ -259,41 +251,17 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -259,41 +251,17 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
bool
get
_isMultiline
=>
config
.
maxLines
>
1
;
bool
get
_isMultiline
=>
config
.
maxLines
>
1
;
double
_contentExtent
=
0.0
;
double
_containerExtent
=
0.0
;
Offset
_handlePaintOffsetUpdateNeeded
(
ViewportDimensions
dimensions
,
Rect
caretRect
)
{
// We make various state changes here but don't have to do so in a
// setState() callback because we are called during layout and all
// we're updating is the new offset, which we are providing to the
// render object via our return value.
_contentExtent
=
_isMultiline
?
dimensions
.
contentSize
.
height
:
dimensions
.
contentSize
.
width
;
_containerExtent
=
_isMultiline
?
dimensions
.
containerSize
.
height
:
dimensions
.
containerSize
.
width
;
didUpdateScrollBehavior
(
scrollBehavior
.
updateExtents
(
contentExtent:
_contentExtent
,
containerExtent:
_containerExtent
,
// TODO(ianh): We should really only do this when text is added,
// not generally any time the size changes.
scrollOffset:
_getScrollOffsetForCaret
(
caretRect
,
_containerExtent
)
));
updateGestureDetector
();
return
scrollOffsetToPixelDelta
(
scrollOffset
);
}
// Calculate the new scroll offset so the cursor remains visible.
// Calculate the new scroll offset so the cursor remains visible.
double
_getScrollOffsetForCaret
(
Rect
caretRect
,
double
containerExtent
)
{
double
_getScrollOffsetForCaret
(
Rect
caretRect
)
{
double
caretStart
=
_isMultiline
?
caretRect
.
top
:
caretRect
.
left
;
final
double
caretStart
=
_isMultiline
?
caretRect
.
top
:
caretRect
.
left
;
double
caretEnd
=
_isMultiline
?
caretRect
.
bottom
:
caretRect
.
right
;
final
double
caretEnd
=
_isMultiline
?
caretRect
.
bottom
:
caretRect
.
right
;
double
newScrollOffset
=
scrollOffset
;
double
scrollOffset
=
_scrollController
.
offset
;
final
double
viewportExtent
=
_scrollController
.
position
.
viewportDimension
;
if
(
caretStart
<
0.0
)
// cursor before start of bounds
if
(
caretStart
<
0.0
)
// cursor before start of bounds
newScrollOffset
+=
pixelOffsetToScrollOffset
(-
caretStart
)
;
scrollOffset
+=
caretStart
;
else
if
(
caretEnd
>=
container
Extent
)
// cursor after end of bounds
else
if
(
caretEnd
>=
viewport
Extent
)
// cursor after end of bounds
newScrollOffset
+=
pixelOffsetToScrollOffset
(-(
caretEnd
-
containerExtent
))
;
scrollOffset
+=
caretEnd
-
viewportExtent
;
return
newS
crollOffset
;
return
s
crollOffset
;
}
}
// True if the focus was explicitly requested last frame. This ensures we
// True if the focus was explicitly requested last frame. This ensures we
...
@@ -302,8 +270,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -302,8 +270,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
void
_attachOrDetachKeyboard
(
bool
focused
)
{
void
_attachOrDetachKeyboard
(
bool
focused
)
{
if
(
focused
&&
!
_isAttachedToKeyboard
&&
(
_requestingFocus
||
config
.
autofocus
))
{
if
(
focused
&&
!
_isAttachedToKeyboard
&&
(
_requestingFocus
||
config
.
autofocus
))
{
_textInputConnection
=
TextInput
.
attach
(
_textInputConnection
=
TextInput
.
attach
(
this
,
new
TextInputConfiguration
(
inputType:
config
.
keyboardType
))
this
,
new
TextInputConfiguration
(
inputType:
config
.
keyboardType
))
..
setEditingState
(
_getTextEditingStateFromInputValue
(
_currentValue
))
..
setEditingState
(
_getTextEditingStateFromInputValue
(
_currentValue
))
..
show
();
..
show
();
}
else
if
(!
focused
)
{
}
else
if
(!
focused
)
{
...
@@ -364,7 +331,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -364,7 +331,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
// EditableWidget, not just changes triggered by user gestures.
// EditableWidget, not just changes triggered by user gestures.
requestKeyboard
();
requestKeyboard
();
InputValue
newInput
=
_currentValue
.
copyWith
(
selection:
selection
,
composing:
TextRange
.
empty
);
final
InputValue
newInput
=
_currentValue
.
copyWith
(
selection:
selection
,
composing:
TextRange
.
empty
);
if
(
config
.
onChanged
!=
null
)
if
(
config
.
onChanged
!=
null
)
config
.
onChanged
(
newInput
);
config
.
onChanged
(
newInput
);
...
@@ -393,14 +360,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -393,14 +360,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
assert
(!
newInput
.
composing
.
isValid
);
// composing range must be empty while selecting
assert
(!
newInput
.
composing
.
isValid
);
// composing range must be empty while selecting
if
(
config
.
onChanged
!=
null
)
if
(
config
.
onChanged
!=
null
)
config
.
onChanged
(
newInput
);
config
.
onChanged
(
newInput
);
_scrollController
.
jumpTo
(
_getScrollOffsetForCaret
(
caretRect
));
didUpdateScrollBehavior
(
scrollBehavior
.
updateExtents
(
// TODO(mpcomplete): should just be able to pass
// scrollBehavior.containerExtent here (and remove the member var), but
// scrollBehavior gets re-created too often, and is sometimes
// uninitialized here. Investigate if this is a bug.
scrollOffset:
_getScrollOffsetForCaret
(
caretRect
,
_containerExtent
)
));
}
}
/// Whether the blinking cursor is actually visible at this precise moment
/// Whether the blinking cursor is actually visible at this precise moment
...
@@ -429,9 +389,12 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -429,9 +389,12 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
_textInputConnection
.
close
();
_textInputConnection
.
close
();
_textInputConnection
=
null
;
_textInputConnection
=
null
;
}
}
assert
(!
_isAttachedToKeyboard
);
if
(
_cursorTimer
!=
null
)
if
(
_cursorTimer
!=
null
)
_stopCursorTimer
();
_stopCursorTimer
();
assert
(
_cursorTimer
==
null
);
_selectionOverlay
?.
dispose
();
_selectionOverlay
?.
dispose
();
_selectionOverlay
=
null
;
super
.
dispose
();
super
.
dispose
();
}
}
...
@@ -442,11 +405,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -442,11 +405,7 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
}
}
@override
@override
Widget
buildContent
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
assert
(
config
.
style
!=
null
);
assert
(
config
.
focusKey
!=
null
);
assert
(
config
.
cursorColor
!=
null
);
bool
focused
=
Focus
.
at
(
config
.
focusKey
.
currentContext
);
bool
focused
=
Focus
.
at
(
config
.
focusKey
.
currentContext
);
_attachOrDetachKeyboard
(
focused
);
_attachOrDetachKeyboard
(
focused
);
...
@@ -464,20 +423,24 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
...
@@ -464,20 +423,24 @@ class EditableTextState extends ScrollableState<EditableText> implements TextInp
}
}
}
}
return
new
ClipRect
(
return
new
Scrollable2
(
child:
new
_Editable
(
axisDirection:
_isMultiline
?
AxisDirection
.
down
:
AxisDirection
.
right
,
value:
_currentValue
,
controller:
_scrollController
,
style:
config
.
style
,
physics:
const
ClampingScrollPhysics
(),
cursorColor:
config
.
cursorColor
,
viewportBuilder:
(
BuildContext
context
,
ViewportOffset
offset
)
{
showCursor:
_showCursor
,
return
new
_Editable
(
maxLines:
config
.
maxLines
,
value:
_currentValue
,
selectionColor:
config
.
selectionColor
,
style:
config
.
style
,
textScaleFactor:
config
.
textScaleFactor
??
MediaQuery
.
of
(
context
).
textScaleFactor
,
cursorColor:
config
.
cursorColor
,
obscureText:
config
.
obscureText
,
showCursor:
_showCursor
,
onSelectionChanged:
_handleSelectionChanged
,
maxLines:
config
.
maxLines
,
paintOffset:
scrollOffsetToPixelDelta
(
scrollOffset
),
selectionColor:
config
.
selectionColor
,
onPaintOffsetUpdateNeeded:
_handlePaintOffsetUpdateNeeded
textScaleFactor:
config
.
textScaleFactor
??
MediaQuery
.
of
(
context
).
textScaleFactor
,
)
obscureText:
config
.
obscureText
,
offset:
offset
,
onSelectionChanged:
_handleSelectionChanged
,
);
},
);
);
}
}
}
}
...
@@ -493,9 +456,8 @@ class _Editable extends LeafRenderObjectWidget {
...
@@ -493,9 +456,8 @@ class _Editable extends LeafRenderObjectWidget {
this
.
selectionColor
,
this
.
selectionColor
,
this
.
textScaleFactor
,
this
.
textScaleFactor
,
this
.
obscureText
,
this
.
obscureText
,
this
.
offset
,
this
.
onSelectionChanged
,
this
.
onSelectionChanged
,
this
.
paintOffset
,
this
.
onPaintOffsetUpdateNeeded
})
:
super
(
key:
key
);
})
:
super
(
key:
key
);
final
InputValue
value
;
final
InputValue
value
;
...
@@ -506,9 +468,8 @@ class _Editable extends LeafRenderObjectWidget {
...
@@ -506,9 +468,8 @@ class _Editable extends LeafRenderObjectWidget {
final
Color
selectionColor
;
final
Color
selectionColor
;
final
double
textScaleFactor
;
final
double
textScaleFactor
;
final
bool
obscureText
;
final
bool
obscureText
;
final
ViewportOffset
offset
;
final
SelectionChangedHandler
onSelectionChanged
;
final
SelectionChangedHandler
onSelectionChanged
;
final
Offset
paintOffset
;
final
RenderEditablePaintOffsetNeededCallback
onPaintOffsetUpdateNeeded
;
@override
@override
RenderEditable
createRenderObject
(
BuildContext
context
)
{
RenderEditable
createRenderObject
(
BuildContext
context
)
{
...
@@ -520,9 +481,8 @@ class _Editable extends LeafRenderObjectWidget {
...
@@ -520,9 +481,8 @@ class _Editable extends LeafRenderObjectWidget {
selectionColor:
selectionColor
,
selectionColor:
selectionColor
,
textScaleFactor:
textScaleFactor
,
textScaleFactor:
textScaleFactor
,
selection:
value
.
selection
,
selection:
value
.
selection
,
offset:
offset
,
onSelectionChanged:
onSelectionChanged
,
onSelectionChanged:
onSelectionChanged
,
paintOffset:
paintOffset
,
onPaintOffsetUpdateNeeded:
onPaintOffsetUpdateNeeded
);
);
}
}
...
@@ -536,9 +496,8 @@ class _Editable extends LeafRenderObjectWidget {
...
@@ -536,9 +496,8 @@ class _Editable extends LeafRenderObjectWidget {
..
selectionColor
=
selectionColor
..
selectionColor
=
selectionColor
..
textScaleFactor
=
textScaleFactor
..
textScaleFactor
=
textScaleFactor
..
selection
=
value
.
selection
..
selection
=
value
.
selection
..
onSelectionChanged
=
onSelectionChanged
..
offset
=
offset
..
paintOffset
=
paintOffset
..
onSelectionChanged
=
onSelectionChanged
;
..
onPaintOffsetUpdateNeeded
=
onPaintOffsetUpdateNeeded
;
}
}
TextSpan
get
_styledTextSpan
{
TextSpan
get
_styledTextSpan
{
...
...
packages/flutter/lib/src/widgets/single_child_scroll_view.dart
View file @
527fddc6
...
@@ -184,13 +184,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
...
@@ -184,13 +184,13 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
@override
@override
bool
get
isRepaintBoundary
=>
true
;
bool
get
isRepaintBoundary
=>
true
;
double
get
_
effective
Extent
{
double
get
_
viewport
Extent
{
assert
(
hasSize
);
assert
(
hasSize
);
switch
(
axis
)
{
switch
(
axis
)
{
case
Axis
.
vertical
:
return
size
.
height
;
case
Axis
.
horizontal
:
case
Axis
.
horizontal
:
return
size
.
width
;
return
size
.
width
;
case
Axis
.
vertical
:
return
size
.
height
;
}
}
return
null
;
return
null
;
}
}
...
@@ -265,7 +265,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
...
@@ -265,7 +265,7 @@ class _RenderSingleChildViewport extends RenderBox with RenderObjectWithChildMix
size
=
constraints
.
constrain
(
child
.
size
);
size
=
constraints
.
constrain
(
child
.
size
);
}
}
offset
.
applyViewportDimension
(
_
effective
Extent
);
offset
.
applyViewportDimension
(
_
viewport
Extent
);
offset
.
applyContentDimensions
(
_minScrollExtent
,
_maxScrollExtent
);
offset
.
applyContentDimensions
(
_minScrollExtent
,
_maxScrollExtent
);
}
}
...
...
packages/flutter/test/widgets/input_test.dart
View file @
527fddc6
...
@@ -581,7 +581,6 @@ void main() {
...
@@ -581,7 +581,6 @@ void main() {
expect
(
inputValue
.
text
,
cutValue
);
expect
(
inputValue
.
text
,
cutValue
);
},
skip:
Platform
.
isMacOS
);
// Skip due to https://github.com/flutter/flutter/issues/6961
},
skip:
Platform
.
isMacOS
);
// Skip due to https://github.com/flutter/flutter/issues/6961
testWidgets
(
'Can scroll multiline input'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'Can scroll multiline input'
,
(
WidgetTester
tester
)
async
{
GlobalKey
inputKey
=
new
GlobalKey
();
GlobalKey
inputKey
=
new
GlobalKey
();
InputValue
inputValue
=
InputValue
.
empty
;
InputValue
inputValue
=
InputValue
.
empty
;
...
@@ -640,6 +639,7 @@ void main() {
...
@@ -640,6 +639,7 @@ void main() {
// Now the first line is scrolled up, and the fourth line is visible.
// Now the first line is scrolled up, and the fourth line is visible.
Point
newFirstPos
=
textOffsetToPosition
(
tester
,
kFourLines
.
indexOf
(
'First'
));
Point
newFirstPos
=
textOffsetToPosition
(
tester
,
kFourLines
.
indexOf
(
'First'
));
Point
newFourthPos
=
textOffsetToPosition
(
tester
,
kFourLines
.
indexOf
(
'Fourth'
));
Point
newFourthPos
=
textOffsetToPosition
(
tester
,
kFourLines
.
indexOf
(
'Fourth'
));
expect
(
newFirstPos
.
y
,
lessThan
(
firstPos
.
y
));
expect
(
newFirstPos
.
y
,
lessThan
(
firstPos
.
y
));
expect
(
inputBox
.
hitTest
(
new
HitTestResult
(),
position:
inputBox
.
globalToLocal
(
newFirstPos
)),
isFalse
);
expect
(
inputBox
.
hitTest
(
new
HitTestResult
(),
position:
inputBox
.
globalToLocal
(
newFirstPos
)),
isFalse
);
expect
(
inputBox
.
hitTest
(
new
HitTestResult
(),
position:
inputBox
.
globalToLocal
(
newFourthPos
)),
isTrue
);
expect
(
inputBox
.
hitTest
(
new
HitTestResult
(),
position:
inputBox
.
globalToLocal
(
newFourthPos
)),
isTrue
);
...
...
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