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
f9a578dd
Unverified
Commit
f9a578dd
authored
Aug 10, 2023
by
Ian Hickson
Committed by
GitHub
Aug 10, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
An example of parentData usage. (#131818)
parent
e972d5a3
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
447 additions
and
18 deletions
+447
-18
parent_data.0.dart
examples/api/lib/rendering/box/parent_data.0.dart
+394
-0
parent_data.0_test.dart
examples/api/test/rendering/box/parent_data.0_test.dart
+17
-0
box.dart
packages/flutter/lib/src/rendering/box.dart
+9
-0
framework.dart
packages/flutter/lib/src/widgets/framework.dart
+27
-18
No files found.
examples/api/lib/rendering/box/parent_data.0.dart
0 → 100644
View file @
f9a578dd
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
void
main
(
)
=>
runApp
(
const
SampleApp
());
class
SampleApp
extends
StatefulWidget
{
const
SampleApp
({
super
.
key
});
@override
State
<
SampleApp
>
createState
()
=>
_SampleAppState
();
}
class
_SampleAppState
extends
State
<
SampleApp
>
{
// This can be toggled using buttons in the UI to change which layout render object is used.
bool
_compact
=
false
;
// This is the content we show in the rendering.
//
// Headline and Paragraph are simple custom widgets defined below.
//
// Any widget _could_ be specified here, and would render fine.
// The Headline and Paragraph widgets are used so that the renderer
// can distinguish between the kinds of content and use different
// spacing between different children.
static
const
List
<
Widget
>
body
=
<
Widget
>[
Headline
(
'Bugs that improve T for future bugs'
),
Paragraph
(
'The best bugs to fix are those that make us more productive '
'in the future. Reducing test flakiness, reducing technical '
'debt, increasing the number of team members who are able to '
'review code confidently and well: this all makes future bugs '
'easier to fix, which is a huge multiplier to our overall '
'effectiveness and thus to developer happiness.'
,
),
Headline
(
'Bugs affecting more people are more valuable (maximize N)'
),
Paragraph
(
'We will make more people happier if we fix a bug experienced by more people.'
),
Paragraph
(
'One thing to be careful about is to think about the number of '
'people we are ignoring in our metrics. For example, if we had '
'a bug that prevented our product from working on Windows, we '
'would have no Windows users, so the bug would affect nobody. '
'However, fixing the bug would enable millions of developers '
"to use our product, and that's the number that counts."
),
Headline
(
'Bugs with greater impact on developers are more valuable (maximize ΔH)'
),
Paragraph
(
'A slight improvement to the user experience is less valuable '
'than a greater improvement. For example, if our application, '
'under certain conditions, shows a message with a typo, and '
'then crashes because of an off-by-one error in the code, '
'fixing the crash is a higher priority than fixing the typo.'
),
];
// This is the description of the demo's interface.
@override
Widget
build
(
BuildContext
context
)
{
return
MaterialApp
(
home:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Custom Render Boxes'
),
// There are two buttons over to the top right of the demo that let you
// toggle between the two rendering modes.
actions:
<
Widget
>[
IconButton
(
icon:
const
Icon
(
Icons
.
density_small
),
isSelected:
_compact
,
onPressed:
()
{
setState
(()
{
_compact
=
true
;
});
},
),
IconButton
(
icon:
const
Icon
(
Icons
.
density_large
),
isSelected:
!
_compact
,
onPressed:
()
{
setState
(()
{
_compact
=
false
;
});
},
),
],
),
body:
SingleChildScrollView
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
30.0
,
vertical:
20.0
),
// CompactLayout and OpenLayout are the two rendering widgets defined below.
child:
_compact
?
const
CompactLayout
(
children:
body
)
:
const
OpenLayout
(
children:
body
),
),
),
);
}
}
// Headline and Paragraph are just wrappers around the Text widget, but they
// also introduce a TextCategory widget that the CompactLayout and OpenLayout
// widgets can read to determine what kind of child is being rendered.
class
Headline
extends
StatelessWidget
{
const
Headline
(
this
.
text
,
{
super
.
key
});
final
String
text
;
@override
Widget
build
(
BuildContext
context
)
{
return
TextCategory
(
category:
'headline'
,
child:
Text
(
text
,
style:
Theme
.
of
(
context
).
textTheme
.
titleLarge
),
);
}
}
class
Paragraph
extends
StatelessWidget
{
const
Paragraph
(
this
.
text
,
{
super
.
key
});
final
String
text
;
@override
Widget
build
(
BuildContext
context
)
{
return
TextCategory
(
category:
'paragraph'
,
child:
Text
(
text
,
style:
Theme
.
of
(
context
).
textTheme
.
bodyLarge
),
);
}
}
// This is the ParentDataWidget that allows us to specify what kind of child
// is being rendered. It allows information to be shared with the render object
// without violating the principle of agnostic composition (wherein parents should
// work with any child, not only support a fixed set of children).
class
TextCategory
extends
ParentDataWidget
<
TextFlowParentData
>
{
const
TextCategory
({
super
.
key
,
required
this
.
category
,
required
super
.
child
});
final
String
category
;
@override
void
applyParentData
(
RenderObject
renderObject
)
{
final
TextFlowParentData
parentData
=
renderObject
.
parentData
!
as
TextFlowParentData
;
if
(
parentData
.
category
!=
category
)
{
parentData
.
category
=
category
;
renderObject
.
parent
!.
markNeedsLayout
();
}
}
@override
Type
get
debugTypicalAncestorWidgetClass
=>
OpenLayout
;
}
// This is one of the two layout variants. It is a widget that defers to
// a render object defined below (RenderCompactLayout).
class
CompactLayout
extends
MultiChildRenderObjectWidget
{
const
CompactLayout
({
super
.
key
,
super
.
children
});
@override
RenderCompactLayout
createRenderObject
(
BuildContext
context
)
{
return
RenderCompactLayout
();
}
@override
void
updateRenderObject
(
BuildContext
context
,
RenderCompactLayout
renderObject
)
{
// nothing to update
}
}
// This is the other of the two layout variants. It is a widget that defers to a
// render object defined below (RenderOpenLayout).
class
OpenLayout
extends
MultiChildRenderObjectWidget
{
const
OpenLayout
({
super
.
key
,
super
.
children
});
@override
RenderOpenLayout
createRenderObject
(
BuildContext
context
)
{
return
RenderOpenLayout
();
}
@override
void
updateRenderObject
(
BuildContext
context
,
RenderOpenLayout
renderObject
)
{
// nothing to update
}
}
// This is the data structure that contains the kind of data that can be
// passed to the parent to label the child. It is literally stored on
// the RenderObject child, in its "parentData" field.
class
TextFlowParentData
extends
ContainerBoxParentData
<
RenderBox
>
{
String
category
=
''
;
}
// This is the bulk of the layout logic. (It's similar to RenderListBody,
// but only supports vertical layout.) It has no properties.
//
// This is an abstract class that is then extended by RenderCompactLayout and
// RenderOpenLayout to get different layouts based on the children's categories,
// as stored in the ParentData structure defined above.
//
// The documentation for the RenderBox class and its members provides much
// more detail on how to implement each of the methods below.
abstract
class
RenderTextFlow
extends
RenderBox
with
ContainerRenderObjectMixin
<
RenderBox
,
TextFlowParentData
>,
RenderBoxContainerDefaultsMixin
<
RenderBox
,
TextFlowParentData
>
{
RenderTextFlow
({
List
<
RenderBox
>?
children
})
{
addAll
(
children
);
}
@override
void
setupParentData
(
RenderBox
child
)
{
if
(
child
.
parentData
is
!
TextFlowParentData
)
{
child
.
parentData
=
TextFlowParentData
();
}
}
// This is the function that is overridden by the subclasses to do the
// actual decision about the space to use between children.
double
spacingBetween
(
String
before
,
String
after
);
// The next few functions are the layout functions. In each case we walk the
// children, calling each one to determine the geometry of the child, and use
// that to determine the layout.
// The first two functions compute the intrinsic width of the render object,
// as seen when using the IntrinsicWidth widget.
//
// They essentially defer to the widest child.
@override
double
computeMinIntrinsicWidth
(
double
height
)
{
double
width
=
0.0
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
final
double
childWidth
=
child
.
getMinIntrinsicWidth
(
height
);
if
(
childWidth
>
width
)
{
width
=
childWidth
;
}
child
=
childAfter
(
child
);
}
return
width
;
}
@override
double
computeMaxIntrinsicWidth
(
double
height
)
{
double
width
=
0.0
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
final
double
childWidth
=
child
.
getMaxIntrinsicWidth
(
height
);
if
(
childWidth
>
width
)
{
width
=
childWidth
;
}
child
=
childAfter
(
child
);
}
return
width
;
}
// The next two functions compute the intrinsic height of the render object,
// as seen when using the IntrinsicHeight widget.
//
// They add up the height contributed by each child.
//
// They have to take into account the categories of the children and the
// spacing that will be added, hence the slightly more elaborate logic.
@override
double
computeMinIntrinsicHeight
(
double
width
)
{
String
?
previousCategory
;
double
height
=
0.0
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
final
String
category
=
(
child
.
parentData
!
as
TextFlowParentData
).
category
;
if
(
previousCategory
!=
null
)
{
height
+=
spacingBetween
(
previousCategory
,
category
);
}
height
+=
child
.
getMinIntrinsicHeight
(
width
);
previousCategory
=
category
;
child
=
childAfter
(
child
);
}
return
height
;
}
@override
double
computeMaxIntrinsicHeight
(
double
width
)
{
String
?
previousCategory
;
double
height
=
0.0
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
final
String
category
=
(
child
.
parentData
!
as
TextFlowParentData
).
category
;
if
(
previousCategory
!=
null
)
{
height
+=
spacingBetween
(
previousCategory
,
category
);
}
height
+=
child
.
getMaxIntrinsicHeight
(
width
);
previousCategory
=
category
;
child
=
childAfter
(
child
);
}
return
height
;
}
// This function implements the baseline logic. Because this class does
// nothing special, we just defer to the default implementation in the
// RenderBoxContainerDefaultsMixin utility class.
@override
double
?
computeDistanceToActualBaseline
(
TextBaseline
baseline
)
{
return
defaultComputeDistanceToFirstActualBaseline
(
baseline
);
}
// Next we have a function similar to the intrinsic methods, but for both axes
// at the same time.
@override
Size
computeDryLayout
(
BoxConstraints
constraints
)
{
final
BoxConstraints
innerConstraints
=
BoxConstraints
.
tightFor
(
width:
constraints
.
maxWidth
);
String
?
previousCategory
;
double
y
=
0.0
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
final
String
category
=
(
child
.
parentData
!
as
TextFlowParentData
).
category
;
if
(
previousCategory
!=
null
)
{
y
+=
spacingBetween
(
previousCategory
,
category
);
}
final
Size
childSize
=
child
.
getDryLayout
(
innerConstraints
);
y
+=
childSize
.
height
;
previousCategory
=
category
;
child
=
childAfter
(
child
);
}
return
constraints
.
constrain
(
Size
(
constraints
.
maxWidth
,
y
));
}
// This is the core of the layout logic. Most of the time, this is the only
// function that will be called. It computes the size and position of each
// child, and stores it (in the parent data, as it happens!) for use during
// the paint phase.
@override
void
performLayout
()
{
final
BoxConstraints
innerConstraints
=
BoxConstraints
.
tightFor
(
width:
constraints
.
maxWidth
);
String
?
previousCategory
;
double
y
=
0.0
;
RenderBox
?
child
=
firstChild
;
while
(
child
!=
null
)
{
final
String
category
=
(
child
.
parentData
!
as
TextFlowParentData
).
category
;
if
(
previousCategory
!=
null
)
{
// This is where we call the function that computes the spacing between
// the different children. The arguments are the categories, obtained
// from the parentData property of each child.
y
+=
spacingBetween
(
previousCategory
,
category
);
}
child
.
layout
(
innerConstraints
,
parentUsesSize:
true
);
(
child
.
parentData
!
as
TextFlowParentData
).
offset
=
Offset
(
0.0
,
y
);
y
+=
child
.
size
.
height
;
previousCategory
=
category
;
child
=
childAfter
(
child
);
}
size
=
constraints
.
constrain
(
Size
(
constraints
.
maxWidth
,
y
));
}
// Hit testing is normal for this widget, so we defer to the default implementation.
@override
bool
hitTestChildren
(
BoxHitTestResult
result
,
{
required
Offset
position
})
{
return
defaultHitTestChildren
(
result
,
position:
position
);
}
// Painting is normal for this widget, so we defer to the default
// implementation. The default implementation expects to find the positions
// configured in the parentData property of each child, which is why we
// configure it that way in performLayout above.
@override
void
paint
(
PaintingContext
context
,
Offset
offset
)
{
defaultPaint
(
context
,
offset
);
}
}
// Finally we have the two render objects that implement the two layouts in this demo.
class
RenderOpenLayout
extends
RenderTextFlow
{
@override
double
spacingBetween
(
String
before
,
String
after
)
{
if
(
after
==
'headline'
)
{
return
20.0
;
}
if
(
before
==
'headline'
)
{
return
5.0
;
}
return
10.0
;
}
}
class
RenderCompactLayout
extends
RenderTextFlow
{
@override
double
spacingBetween
(
String
before
,
String
after
)
{
if
(
after
==
'headline'
)
{
return
4.0
;
}
return
2.0
;
}
}
examples/api/test/rendering/box/parent_data.0_test.dart
0 → 100644
View file @
f9a578dd
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/material.dart'
;
import
'package:flutter_api_samples/rendering/box/parent_data.0.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
testWidgets
(
'parent data example'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
SampleApp
());
expect
(
tester
.
getTopLeft
(
find
.
byType
(
Headline
).
at
(
2
)),
const
Offset
(
30.0
,
728.0
));
await
tester
.
tap
(
find
.
byIcon
(
Icons
.
density_small
));
await
tester
.
pump
();
expect
(
tester
.
getTopLeft
(
find
.
byType
(
Headline
).
at
(
2
)),
const
Offset
(
30.0
,
682.0
));
});
}
packages/flutter/lib/src/rendering/box.dart
View file @
f9a578dd
...
@@ -911,6 +911,15 @@ class BoxHitTestEntry extends HitTestEntry<RenderBox> {
...
@@ -911,6 +911,15 @@ class BoxHitTestEntry extends HitTestEntry<RenderBox> {
}
}
/// Parent data used by [RenderBox] and its subclasses.
/// Parent data used by [RenderBox] and its subclasses.
///
/// {@tool dartpad}
/// Parent data is used to communicate to a render object about its
/// children. In this example, there are two render objects that perform
/// text layout. They use parent data to identify the kind of child they
/// are laying out, and space the children accordingly.
///
/// ** See code in examples/api/lib/rendering/box/parent_data.0.dart **
/// {@end-tool}
class
BoxParentData
extends
ParentData
{
class
BoxParentData
extends
ParentData
{
/// The offset at which to paint the child in the parent's coordinate system.
/// The offset at which to paint the child in the parent's coordinate system.
Offset
offset
=
Offset
.
zero
;
Offset
offset
=
Offset
.
zero
;
...
...
packages/flutter/lib/src/widgets/framework.dart
View file @
f9a578dd
...
@@ -1778,16 +1778,20 @@ abstract class InheritedWidget extends ProxyWidget {
...
@@ -1778,16 +1778,20 @@ abstract class InheritedWidget extends ProxyWidget {
bool
updateShouldNotify
(
covariant
InheritedWidget
oldWidget
);
bool
updateShouldNotify
(
covariant
InheritedWidget
oldWidget
);
}
}
///
RenderObjectWidget
s provide the configuration for [RenderObjectElement]s,
///
[RenderObjectWidget]
s provide the configuration for [RenderObjectElement]s,
/// which wrap [RenderObject]s, which provide the actual rendering of the
/// which wrap [RenderObject]s, which provide the actual rendering of the
/// application.
/// application.
///
///
/// See also:
/// Usually, rather than subclassing [RenderObjectWidget] directly, render
/// object widgets subclass one of:
///
///
/// * [MultiChildRenderObjectWidget], which configures a [RenderObject] with
/// * [LeafRenderObjectWidget], if the widget has no children.
/// a single list of children.
/// * [SingleChildRenderObjectElement], if the widget has exactly one child.
/// * [SlottedMultiChildRenderObjectWidget], which configures a
/// * [MultiChildRenderObjectWidget], if the widget takes a list of children.
/// [RenderObject] that organizes its children in different named slots.
/// * [SlottedMultiChildRenderObjectWidget], if the widget organizes its
/// children in different named slots.
///
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
abstract
class
RenderObjectWidget
extends
Widget
{
abstract
class
RenderObjectWidget
extends
Widget
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
/// const constructors so that they can be used in const expressions.
...
@@ -1830,8 +1834,10 @@ abstract class RenderObjectWidget extends Widget {
...
@@ -1830,8 +1834,10 @@ abstract class RenderObjectWidget extends Widget {
void
didUnmountRenderObject
(
covariant
RenderObject
renderObject
)
{
}
void
didUnmountRenderObject
(
covariant
RenderObject
renderObject
)
{
}
}
}
/// A superclass for
RenderObjectWidgets that configure RenderObject
subclasses
/// A superclass for
[RenderObjectWidget]s that configure [RenderObject]
subclasses
/// that have no children.
/// that have no children.
///
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
abstract
class
LeafRenderObjectWidget
extends
RenderObjectWidget
{
abstract
class
LeafRenderObjectWidget
extends
RenderObjectWidget
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
/// const constructors so that they can be used in const expressions.
...
@@ -1842,13 +1848,14 @@ abstract class LeafRenderObjectWidget extends RenderObjectWidget {
...
@@ -1842,13 +1848,14 @@ abstract class LeafRenderObjectWidget extends RenderObjectWidget {
}
}
/// A superclass for [RenderObjectWidget]s that configure [RenderObject] subclasses
/// A superclass for [RenderObjectWidget]s that configure [RenderObject] subclasses
/// that have a single child slot. (This superclass only provides the storage
/// that have a single child slot.
/// for that child, it doesn't actually provide the updating logic.)
///
///
/// T
ypically, the render object assigned to this widget will
make use of
/// T
he render object assigned to this widget should
make use of
/// [RenderObjectWithChildMixin] to implement a single-child model. The mixin
/// [RenderObjectWithChildMixin] to implement a single-child model. The mixin
/// exposes a [RenderObjectWithChildMixin.child] property that allows
/// exposes a [RenderObjectWithChildMixin.child] property that allows retrieving
/// retrieving the render object belonging to the [child] widget.
/// the render object belonging to the [child] widget.
///
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
abstract
class
SingleChildRenderObjectWidget
extends
RenderObjectWidget
{
abstract
class
SingleChildRenderObjectWidget
extends
RenderObjectWidget
{
/// Abstract const constructor. This constructor enables subclasses to provide
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
/// const constructors so that they can be used in const expressions.
...
@@ -1868,13 +1875,15 @@ abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
...
@@ -1868,13 +1875,15 @@ abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
/// storage for that child list, it doesn't actually provide the updating
/// storage for that child list, it doesn't actually provide the updating
/// logic.)
/// logic.)
///
///
/// Subclasses must
return
a [RenderObject] that mixes in
/// Subclasses must
use
a [RenderObject] that mixes in
/// [ContainerRenderObjectMixin], which provides the necessary functionality to
/// [ContainerRenderObjectMixin], which provides the necessary functionality to
/// visit the children of the container render object (the render object
/// visit the children of the container render object (the render object
/// belonging to the [children] widgets). Typically, subclasses will
return
a
/// belonging to the [children] widgets). Typically, subclasses will
use
a
/// [RenderBox] that mixes in both [ContainerRenderObjectMixin] and
/// [RenderBox] that mixes in both [ContainerRenderObjectMixin] and
/// [RenderBoxContainerDefaultsMixin].
/// [RenderBoxContainerDefaultsMixin].
///
///
/// Subclasses must implement [createRenderObject] and [updateRenderObject].
///
/// See also:
/// See also:
///
///
/// * [Stack], which uses [MultiChildRenderObjectWidget].
/// * [Stack], which uses [MultiChildRenderObjectWidget].
...
@@ -6538,8 +6547,8 @@ class LeafRenderObjectElement extends RenderObjectElement {
...
@@ -6538,8 +6547,8 @@ class LeafRenderObjectElement extends RenderObjectElement {
///
///
/// The child is optional.
/// The child is optional.
///
///
/// This element subclass can be used for
RenderObjectWidget
s whose
/// This element subclass can be used for
[RenderObjectWidget]
s whose
///
RenderObject
s use the [RenderObjectWithChildMixin] mixin. Such widgets are
///
[RenderObject]
s use the [RenderObjectWithChildMixin] mixin. Such widgets are
/// expected to inherit from [SingleChildRenderObjectWidget].
/// expected to inherit from [SingleChildRenderObjectWidget].
class
SingleChildRenderObjectElement
extends
RenderObjectElement
{
class
SingleChildRenderObjectElement
extends
RenderObjectElement
{
/// Creates an element that uses the given widget as its configuration.
/// Creates an element that uses the given widget as its configuration.
...
@@ -6600,8 +6609,8 @@ class SingleChildRenderObjectElement extends RenderObjectElement {
...
@@ -6600,8 +6609,8 @@ class SingleChildRenderObjectElement extends RenderObjectElement {
/// An [Element] that uses a [MultiChildRenderObjectWidget] as its configuration.
/// An [Element] that uses a [MultiChildRenderObjectWidget] as its configuration.
///
///
/// This element subclass can be used for
RenderObjectWidget
s whose
/// This element subclass can be used for
[RenderObjectWidget]
s whose
///
RenderObject
s use the [ContainerRenderObjectMixin] mixin with a parent data
///
[RenderObject]
s use the [ContainerRenderObjectMixin] mixin with a parent data
/// type that implements [ContainerParentDataMixin<RenderObject>]. Such widgets
/// type that implements [ContainerParentDataMixin<RenderObject>]. Such widgets
/// are expected to inherit from [MultiChildRenderObjectWidget].
/// are expected to inherit from [MultiChildRenderObjectWidget].
///
///
...
...
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