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
928c41e9
Unverified
Commit
928c41e9
authored
Apr 18, 2018
by
Jacob Richman
Committed by
GitHub
Apr 18, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Support summary-details tree view (#16638)
Support summary-details tree view.
parent
1080c298
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
581 additions
and
29 deletions
+581
-29
widget_inspector.dart
packages/flutter/lib/src/widgets/widget_inspector.dart
+368
-28
service_extensions_test.dart
...ages/flutter/test/foundation/service_extensions_test.dart
+1
-1
widget_inspector_test.dart
packages/flutter/test/widgets/widget_inspector_test.dart
+212
-0
No files found.
packages/flutter/lib/src/widgets/widget_inspector.dart
View file @
928c41e9
...
@@ -20,6 +20,7 @@ import 'basic.dart';
...
@@ -20,6 +20,7 @@ import 'basic.dart';
import
'binding.dart'
;
import
'binding.dart'
;
import
'framework.dart'
;
import
'framework.dart'
;
import
'gesture_detector.dart'
;
import
'gesture_detector.dart'
;
import
'icon_data.dart'
;
/// Signature for the builder callback used by
/// Signature for the builder callback used by
/// [WidgetInspector.selectButtonBuilder].
/// [WidgetInspector.selectButtonBuilder].
...
@@ -103,6 +104,52 @@ class _InspectorReferenceData {
...
@@ -103,6 +104,52 @@ class _InspectorReferenceData {
int
count
=
1
;
int
count
=
1
;
}
}
/// Configuration controlling how [DiagnosticsNode] objects are serialized to
/// JSON mainly focused on if and how children are included in the JSON.
class
_SerializeConfig
{
_SerializeConfig
({
@required
this
.
groupName
,
this
.
summaryTree
:
false
,
this
.
subtreeDepth
:
1
,
this
.
pathToInclude
,
this
.
includeProperties
:
false
,
this
.
expandPropertyValues
:
true
,
});
_SerializeConfig
.
merge
(
_SerializeConfig
base
,
{
int
subtreeDepth
,
bool
omitChildren
,
Iterable
<
Diagnosticable
>
pathToInclude
,
})
:
groupName
=
base
.
groupName
,
summaryTree
=
base
.
summaryTree
,
subtreeDepth
=
subtreeDepth
??
base
.
subtreeDepth
,
pathToInclude
=
pathToInclude
??
base
.
pathToInclude
,
includeProperties
=
base
.
includeProperties
,
expandPropertyValues
=
base
.
expandPropertyValues
;
final
String
groupName
;
/// Whether to only include children that would exist in the summary tree.
final
bool
summaryTree
;
/// How many levels of children to include in the JSON payload.
final
int
subtreeDepth
;
/// Path of nodes through the children of this node to include even if
/// subtreeDepth is exceeded.
final
Iterable
<
Diagnosticable
>
pathToInclude
;
/// Include information about properties in the JSON instead of requiring
/// a separate request to determine properties.
final
bool
includeProperties
;
/// Expand children of properties that have values that are themselves
/// Diagnosticable objects.
final
bool
expandPropertyValues
;
}
class
_WidgetInspectorService
extends
Object
with
WidgetInspectorService
{
class
_WidgetInspectorService
extends
Object
with
WidgetInspectorService
{
}
}
...
@@ -131,6 +178,11 @@ class WidgetInspectorService {
...
@@ -131,6 +178,11 @@ class WidgetInspectorService {
// [instance] for production purposes.
// [instance] for production purposes.
factory
WidgetInspectorService
.
_
()
=>
new
_WidgetInspectorService
();
factory
WidgetInspectorService
.
_
()
=>
new
_WidgetInspectorService
();
/// Ring of cached JSON values to prevent json from being garbage
/// collected before it can be requested over the Observatory protocol.
final
List
<
String
>
_serializeRing
=
new
List
<
String
>(
20
);
int
_serializeRingIndex
=
0
;
/// The current [WidgetInspectorService].
/// The current [WidgetInspectorService].
static
WidgetInspectorService
get
instance
=>
_instance
;
static
WidgetInspectorService
get
instance
=>
_instance
;
static
WidgetInspectorService
_instance
=
new
WidgetInspectorService
.
_
();
static
WidgetInspectorService
_instance
=
new
WidgetInspectorService
.
_
();
...
@@ -368,6 +420,17 @@ class WidgetInspectorService {
...
@@ -368,6 +420,17 @@ class WidgetInspectorService {
name:
'getChildren'
,
name:
'getChildren'
,
callback:
_getChildren
,
callback:
_getChildren
,
);
);
_registerServiceExtensionWithArg
(
name:
'getChildrenSummaryTree'
,
callback:
_getChildrenSummaryTree
,
);
_registerServiceExtensionWithArg
(
name:
'getChildrenDetailsSubtree'
,
callback:
_getChildrenDetailsSubtree
,
);
_registerObjectGroupServiceExtension
(
_registerObjectGroupServiceExtension
(
name:
'getRootWidget'
,
name:
'getRootWidget'
,
callback:
_getRootWidget
,
callback:
_getRootWidget
,
...
@@ -376,6 +439,15 @@ class WidgetInspectorService {
...
@@ -376,6 +439,15 @@ class WidgetInspectorService {
name:
'getRootRenderObject'
,
name:
'getRootRenderObject'
,
callback:
_getRootRenderObject
,
callback:
_getRootRenderObject
,
);
);
_registerObjectGroupServiceExtension
(
name:
'getRootWidgetSummaryTree'
,
callback:
_getRootWidgetSummaryTree
,
);
_registerServiceExtensionWithArg
(
name:
'getDetailsSubtree'
,
callback:
_getDetailsSubtree
,
);
_registerServiceExtensionWithArg
(
_registerServiceExtensionWithArg
(
name:
'getSelectedRenderObject'
,
name:
'getSelectedRenderObject'
,
callback:
_getSelectedRenderObject
,
callback:
_getSelectedRenderObject
,
...
@@ -384,6 +456,11 @@ class WidgetInspectorService {
...
@@ -384,6 +456,11 @@ class WidgetInspectorService {
name:
'getSelectedWidget'
,
name:
'getSelectedWidget'
,
callback:
_getSelectedWidget
,
callback:
_getSelectedWidget
,
);
);
_registerServiceExtensionWithArg
(
name:
'getSelectedSummaryWidget'
,
callback:
_getSelectedSummaryWidget
,
);
_registerSignalServiceExtension
(
_registerSignalServiceExtension
(
name:
'isWidgetCreationTracked'
,
name:
'isWidgetCreationTracked'
,
callback:
isWidgetCreationTracked
,
callback:
isWidgetCreationTracked
,
...
@@ -580,7 +657,7 @@ class WidgetInspectorService {
...
@@ -580,7 +657,7 @@ class WidgetInspectorService {
/// all nodes other than nodes along the path collapsed.
/// all nodes other than nodes along the path collapsed.
@protected
@protected
String
getParentChain
(
String
id
,
String
groupName
)
{
String
getParentChain
(
String
id
,
String
groupName
)
{
return
json
.
e
ncode
(
_getParentChain
(
id
,
groupName
));
return
_safeJsonE
ncode
(
_getParentChain
(
id
,
groupName
));
}
}
List
<
Object
>
_getParentChain
(
String
id
,
String
groupName
)
{
List
<
Object
>
_getParentChain
(
String
id
,
String
groupName
)
{
...
@@ -593,24 +670,45 @@ class WidgetInspectorService {
...
@@ -593,24 +670,45 @@ class WidgetInspectorService {
else
else
throw
new
FlutterError
(
'Cannot get parent chain for node of type
${value.runtimeType}
'
);
throw
new
FlutterError
(
'Cannot get parent chain for node of type
${value.runtimeType}
'
);
return
path
.
map
((
_DiagnosticsPathNode
node
)
=>
_pathNodeToJson
(
node
,
groupName
)).
toList
();
return
path
.
map
((
_DiagnosticsPathNode
node
)
=>
_pathNodeToJson
(
node
,
new
_SerializeConfig
(
groupName:
groupName
),
)).
toList
();
}
}
Map
<
String
,
Object
>
_pathNodeToJson
(
_DiagnosticsPathNode
pathNode
,
String
groupName
)
{
Map
<
String
,
Object
>
_pathNodeToJson
(
_DiagnosticsPathNode
pathNode
,
_SerializeConfig
config
)
{
if
(
pathNode
==
null
)
if
(
pathNode
==
null
)
return
null
;
return
null
;
return
<
String
,
Object
>{
return
<
String
,
Object
>{
'node'
:
_nodeToJson
(
pathNode
.
node
,
groupName
),
'node'
:
_nodeToJson
(
pathNode
.
node
,
config
),
'children'
:
_nodesToJson
(
pathNode
.
children
,
groupName
),
'children'
:
_nodesToJson
(
pathNode
.
children
,
config
),
'childIndex'
:
pathNode
.
childIndex
,
'childIndex'
:
pathNode
.
childIndex
,
};
};
}
}
List
<
_DiagnosticsPathNode
>
_getElementParentChain
(
Element
element
,
String
groupName
)
{
List
<
Element
>
_getRawElementParentChain
(
Element
element
,
{
int
numLocalParents
})
{
return
_followDiagnosticableChain
(
element
?.
debugGetDiagnosticChain
()?.
reversed
?.
toList
())
??
const
<
_DiagnosticsPathNode
>[];
List
<
Element
>
elements
=
element
?.
debugGetDiagnosticChain
();
if
(
numLocalParents
!=
null
)
{
for
(
int
i
=
0
;
i
<
elements
.
length
;
i
+=
1
)
{
if
(
_isValueCreatedByLocalProject
(
elements
[
i
]))
{
numLocalParents
--;
if
(
numLocalParents
<=
0
)
{
elements
=
elements
.
take
(
i
+
1
).
toList
();
break
;
}
}
}
}
return
elements
?.
reversed
?.
toList
();
}
List
<
_DiagnosticsPathNode
>
_getElementParentChain
(
Element
element
,
String
groupName
,
{
int
numLocalParents
})
{
return
_followDiagnosticableChain
(
_getRawElementParentChain
(
element
,
numLocalParents:
numLocalParents
),
)
??
const
<
_DiagnosticsPathNode
>[];
}
}
List
<
_DiagnosticsPathNode
>
_getRenderObjectParentChain
(
RenderObject
renderObject
,
String
groupName
)
{
List
<
_DiagnosticsPathNode
>
_getRenderObjectParentChain
(
RenderObject
renderObject
,
String
groupName
,
{
int
maxparents
}
)
{
final
List
<
RenderObject
>
chain
=
<
RenderObject
>[];
final
List
<
RenderObject
>
chain
=
<
RenderObject
>[];
while
(
renderObject
!=
null
)
{
while
(
renderObject
!=
null
)
{
chain
.
add
(
renderObject
);
chain
.
add
(
renderObject
);
...
@@ -619,25 +717,84 @@ class WidgetInspectorService {
...
@@ -619,25 +717,84 @@ class WidgetInspectorService {
return
_followDiagnosticableChain
(
chain
.
reversed
.
toList
());
return
_followDiagnosticableChain
(
chain
.
reversed
.
toList
());
}
}
Map
<
String
,
Object
>
_nodeToJson
(
DiagnosticsNode
node
,
String
groupName
)
{
Map
<
String
,
Object
>
_nodeToJson
(
DiagnosticsNode
node
,
_SerializeConfig
config
,
)
{
if
(
node
==
null
)
if
(
node
==
null
)
return
null
;
return
null
;
final
Map
<
String
,
Object
>
json
=
node
.
toJsonMap
();
final
Map
<
String
,
Object
>
json
=
node
.
toJsonMap
();
json
[
'objectId'
]
=
toId
(
node
,
groupName
);
json
[
'objectId'
]
=
toId
(
node
,
config
.
groupName
);
final
Object
value
=
node
.
value
;
final
Object
value
=
node
.
value
;
json
[
'valueId'
]
=
toId
(
value
,
groupName
);
json
[
'valueId'
]
=
toId
(
value
,
config
.
groupName
);
if
(
config
.
summaryTree
)
{
json
[
'summaryTree'
]
=
true
;
}
final
_Location
creationLocation
=
_getCreationLocation
(
value
);
final
_Location
creationLocation
=
_getCreationLocation
(
value
);
bool
createdByLocalProject
=
false
;
if
(
creationLocation
!=
null
)
{
if
(
creationLocation
!=
null
)
{
json
[
'creationLocation'
]
=
creationLocation
.
toJsonMap
();
json
[
'creationLocation'
]
=
creationLocation
.
toJsonMap
();
if
(
_isLocalCreationLocation
(
creationLocation
))
{
if
(
_isLocalCreationLocation
(
creationLocation
))
{
createdByLocalProject
=
true
;
json
[
'createdByLocalProject'
]
=
true
;
json
[
'createdByLocalProject'
]
=
true
;
}
}
}
}
if
(
config
.
subtreeDepth
>
0
||
(
config
.
pathToInclude
!=
null
&&
config
.
pathToInclude
.
isNotEmpty
))
{
json
[
'children'
]
=
_nodesToJson
(
_getChildrenHelper
(
node
,
config
),
config
);
}
if
(
config
.
includeProperties
)
{
json
[
'properties'
]
=
_nodesToJson
(
node
.
getProperties
().
where
(
(
DiagnosticsNode
node
)
=>
!
node
.
isFiltered
(
createdByLocalProject
?
DiagnosticLevel
.
fine
:
DiagnosticLevel
.
info
),
),
new
_SerializeConfig
(
groupName:
config
.
groupName
,
subtreeDepth:
1
,
expandPropertyValues:
true
),
);
}
if
(
node
is
DiagnosticsProperty
)
{
// Add additional information about properties needed for graphical
// display of properties.
if
(
value
is
Color
)
{
json
[
'valueProperties'
]
=
<
String
,
Object
>{
'red'
:
value
.
red
,
'green'
:
value
.
green
,
'blue'
:
value
.
blue
,
'alpha'
:
value
.
alpha
,
};
}
else
if
(
value
is
IconData
)
{
json
[
'valueProperties'
]
=
<
String
,
Object
>{
'codePoint'
:
value
.
codePoint
,
};
}
if
(
config
.
expandPropertyValues
&&
value
is
Diagnosticable
)
{
json
[
'properties'
]
=
_nodesToJson
(
value
.
toDiagnosticsNode
().
getProperties
().
where
(
(
DiagnosticsNode
node
)
=>
!
node
.
isFiltered
(
DiagnosticLevel
.
info
),
),
new
_SerializeConfig
(
groupName:
config
.
groupName
,
subtreeDepth:
0
,
expandPropertyValues:
false
,
),
);
}
}
return
json
;
return
json
;
}
}
bool
_isValueCreatedByLocalProject
(
Object
value
)
{
final
_Location
creationLocation
=
_getCreationLocation
(
value
);
if
(
creationLocation
==
null
)
{
return
false
;
}
return
_isLocalCreationLocation
(
creationLocation
);
}
bool
_isLocalCreationLocation
(
_Location
location
)
{
bool
_isLocalCreationLocation
(
_Location
location
)
{
if
(
_pubRootDirectories
==
null
||
location
==
null
||
location
.
file
==
null
)
{
if
(
_pubRootDirectories
==
null
||
location
==
null
||
location
.
file
==
null
)
{
return
false
;
return
false
;
...
@@ -651,58 +808,205 @@ class WidgetInspectorService {
...
@@ -651,58 +808,205 @@ class WidgetInspectorService {
return
false
;
return
false
;
}
}
Map
<
String
,
Object
>
_serializeToJson
(
DiagnosticsNode
node
,
String
groupName
)
{
/// Wrapper around `json.encode` that uses a ring of cached values to prevent
return
_nodeToJson
(
node
,
groupName
);
/// the Dart garbage collector from collecting objects between when
/// the value is returned over the Observatory protocol and when the
/// separate observatory protocol command has to be used to retrieve its full
/// contents.
/// TODO(jacobr): Replace this with a better solution once
/// https://github.com/dart-lang/sdk/issues/32919 is fixed.
String
_safeJsonEncode
(
Object
object
)
{
final
String
jsonString
=
json
.
encode
(
object
);
_serializeRing
[
_serializeRingIndex
]
=
jsonString
;
_serializeRingIndex
=
(
_serializeRingIndex
+
1
)
%
_serializeRing
.
length
;
return
jsonString
;
}
}
List
<
Map
<
String
,
Object
>>
_nodesToJson
(
Iterable
<
DiagnosticsNode
>
nodes
,
String
groupName
)
{
List
<
Map
<
String
,
Object
>>
_nodesToJson
(
Iterable
<
DiagnosticsNode
>
nodes
,
_SerializeConfig
config
,
)
{
if
(
nodes
==
null
)
if
(
nodes
==
null
)
return
<
Map
<
String
,
Object
>>[];
return
<
Map
<
String
,
Object
>>[];
return
nodes
.
map
<
Map
<
String
,
Object
>>((
DiagnosticsNode
node
)
=>
_nodeToJson
(
node
,
groupName
)).
toList
();
return
nodes
.
map
<
Map
<
String
,
Object
>>(
(
DiagnosticsNode
node
)
{
if
(
config
.
pathToInclude
!=
null
&&
config
.
pathToInclude
.
isNotEmpty
)
{
if
(
config
.
pathToInclude
.
first
==
node
.
value
)
{
return
_nodeToJson
(
node
,
new
_SerializeConfig
.
merge
(
config
,
pathToInclude:
config
.
pathToInclude
.
skip
(
1
)),
);
}
else
{
return
_nodeToJson
(
node
,
new
_SerializeConfig
.
merge
(
config
,
omitChildren:
true
));
}
}
// The tricky special case here is that when in the detailsTree,
// we keep subtreeDepth from going down to zero until we reach nodes
// that also exist in the summary tree. This ensures that every time
// you expand a node in the details tree, you expand the entire subtree
// up until you reach the next nodes shared with the summary tree.
return
_nodeToJson
(
node
,
config
.
summaryTree
||
config
.
subtreeDepth
>
1
||
_shouldShowInSummaryTree
(
node
)
?
new
_SerializeConfig
.
merge
(
config
,
subtreeDepth:
config
.
subtreeDepth
-
1
)
:
config
,
);
}).
toList
();
}
}
/// Returns a JSON representation of the properties of the [DiagnosticsNode]
/// Returns a JSON representation of the properties of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references.
/// object that `diagnosticsNodeId` references.
@protected
@protected
String
getProperties
(
String
diagnosticsNodeId
,
String
groupName
)
{
String
getProperties
(
String
diagnosticsNodeId
,
String
groupName
)
{
return
json
.
e
ncode
(
_getProperties
(
diagnosticsNodeId
,
groupName
));
return
_safeJsonE
ncode
(
_getProperties
(
diagnosticsNodeId
,
groupName
));
}
}
List
<
Object
>
_getProperties
(
String
diagnosticsNodeId
,
String
groupName
)
{
List
<
Object
>
_getProperties
(
String
diagnosticsNodeId
,
String
groupName
)
{
final
DiagnosticsNode
node
=
toObject
(
diagnosticsNodeId
);
final
DiagnosticsNode
node
=
toObject
(
diagnosticsNodeId
);
return
_nodesToJson
(
node
==
null
?
const
<
DiagnosticsNode
>[]
:
node
.
getProperties
(),
groupName
);
return
_nodesToJson
(
node
==
null
?
const
<
DiagnosticsNode
>[]
:
node
.
getProperties
(),
new
_SerializeConfig
(
groupName:
groupName
)
);
}
}
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references.
/// object that `diagnosticsNodeId` references.
String
getChildren
(
String
diagnosticsNodeId
,
String
groupName
)
{
String
getChildren
(
String
diagnosticsNodeId
,
String
groupName
)
{
return
json
.
e
ncode
(
_getChildren
(
diagnosticsNodeId
,
groupName
));
return
_safeJsonE
ncode
(
_getChildren
(
diagnosticsNodeId
,
groupName
));
}
}
List
<
Object
>
_getChildren
(
String
diagnosticsNodeId
,
String
groupName
)
{
List
<
Object
>
_getChildren
(
String
diagnosticsNodeId
,
String
groupName
)
{
final
DiagnosticsNode
node
=
toObject
(
diagnosticsNodeId
);
final
DiagnosticsNode
node
=
toObject
(
diagnosticsNodeId
);
return
_nodesToJson
(
node
==
null
?
const
<
DiagnosticsNode
>[]
:
node
.
getChildren
(),
groupName
);
final
_SerializeConfig
config
=
new
_SerializeConfig
(
groupName:
groupName
);
return
_nodesToJson
(
node
==
null
?
const
<
DiagnosticsNode
>[]
:
_getChildrenHelper
(
node
,
config
),
config
);
}
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references only including children that
/// were created directly by user code.
///
/// Requires [Widget] creation locations which are only available for debug
/// mode builds when the `--track-widget-creation` flag is passed to
/// `flutter_tool`.
///
/// See also:
///
/// * [isWidgetCreationTracked] which indicates whether this method can be
/// used.
String
getChildrenSummaryTree
(
String
diagnosticsNodeId
,
String
groupName
)
{
return
_safeJsonEncode
(
_getChildrenSummaryTree
(
diagnosticsNodeId
,
groupName
));
}
List
<
Object
>
_getChildrenSummaryTree
(
String
diagnosticsNodeId
,
String
groupName
)
{
final
DiagnosticsNode
node
=
toObject
(
diagnosticsNodeId
);
final
_SerializeConfig
config
=
new
_SerializeConfig
(
groupName:
groupName
,
summaryTree:
true
);
return
_nodesToJson
(
node
==
null
?
const
<
DiagnosticsNode
>[]
:
_getChildrenHelper
(
node
,
config
),
config
);
}
/// Returns a JSON representation of the children of the [DiagnosticsNode]
/// object that `diagnosticsNodeId` references providing information needed
/// for the details subtree view.
///
/// The details subtree shows properties inline and includes all children
/// rather than a filtered set of important children.
String
getChildrenDetailsSubtree
(
String
diagnosticsNodeId
,
String
groupName
)
{
return
_safeJsonEncode
(
_getChildrenDetailsSubtree
(
diagnosticsNodeId
,
groupName
));
}
List
<
Object
>
_getChildrenDetailsSubtree
(
String
diagnosticsNodeId
,
String
groupName
)
{
final
DiagnosticsNode
node
=
toObject
(
diagnosticsNodeId
);
// With this value of minDepth we only expand one extra level of important nodes.
final
_SerializeConfig
config
=
new
_SerializeConfig
(
groupName:
groupName
,
subtreeDepth:
1
,
includeProperties:
true
);
return
_nodesToJson
(
node
==
null
?
const
<
DiagnosticsNode
>[]
:
_getChildrenHelper
(
node
,
config
),
config
);
}
List
<
DiagnosticsNode
>
_getChildrenHelper
(
DiagnosticsNode
node
,
_SerializeConfig
config
)
{
return
_getChildrenFiltered
(
node
,
config
).
toList
();
}
bool
_shouldShowInSummaryTree
(
DiagnosticsNode
node
)
{
final
Object
value
=
node
.
value
;
if
(
value
is
!
Diagnosticable
)
{
return
true
;
}
if
(
value
is
!
Element
||
!
isWidgetCreationTracked
())
{
// Creation locations are not availabe so include all nodes in the
// summary tree.
return
true
;
}
return
_isValueCreatedByLocalProject
(
value
);
}
List
<
DiagnosticsNode
>
_getChildrenFiltered
(
DiagnosticsNode
node
,
_SerializeConfig
config
,
)
{
final
List
<
DiagnosticsNode
>
children
=
<
DiagnosticsNode
>[];
for
(
DiagnosticsNode
child
in
node
.
getChildren
())
{
if
(!
config
.
summaryTree
||
_shouldShowInSummaryTree
(
child
))
{
children
.
add
(
child
);
}
else
{
children
.
addAll
(
_getChildrenFiltered
(
child
,
config
));
}
}
return
children
;
}
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [Element].
/// [Element].
String
getRootWidget
(
String
groupName
)
{
String
getRootWidget
(
String
groupName
)
{
return
json
.
e
ncode
(
_getRootWidget
(
groupName
));
return
_safeJsonE
ncode
(
_getRootWidget
(
groupName
));
}
}
Map
<
String
,
Object
>
_getRootWidget
(
String
groupName
)
{
Map
<
String
,
Object
>
_getRootWidget
(
String
groupName
)
{
return
_serializeToJson
(
WidgetsBinding
.
instance
?.
renderViewElement
?.
toDiagnosticsNode
(),
groupName
);
return
_nodeToJson
(
WidgetsBinding
.
instance
?.
renderViewElement
?.
toDiagnosticsNode
(),
new
_SerializeConfig
(
groupName:
groupName
));
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [Element] showing only nodes that should be included in a summary tree.
String
getRootWidgetSummaryTree
(
String
groupName
)
{
return
_safeJsonEncode
(
_getRootWidgetSummaryTree
(
groupName
));
}
Map
<
String
,
Object
>
_getRootWidgetSummaryTree
(
String
groupName
)
{
return
_nodeToJson
(
WidgetsBinding
.
instance
?.
renderViewElement
?.
toDiagnosticsNode
(),
new
_SerializeConfig
(
groupName:
groupName
,
subtreeDepth:
1000000
,
summaryTree:
true
),
);
}
}
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// Returns a JSON representation of the [DiagnosticsNode] for the root
/// [RenderObject].
/// [RenderObject].
@protected
@protected
String
getRootRenderObject
(
String
groupName
)
{
String
getRootRenderObject
(
String
groupName
)
{
return
json
.
e
ncode
(
_getRootRenderObject
(
groupName
));
return
_safeJsonE
ncode
(
_getRootRenderObject
(
groupName
));
}
}
Map
<
String
,
Object
>
_getRootRenderObject
(
String
groupName
)
{
Map
<
String
,
Object
>
_getRootRenderObject
(
String
groupName
)
{
return
_serializeToJson
(
RendererBinding
.
instance
?.
renderView
?.
toDiagnosticsNode
(),
groupName
);
return
_nodeToJson
(
RendererBinding
.
instance
?.
renderView
?.
toDiagnosticsNode
(),
new
_SerializeConfig
(
groupName:
groupName
));
}
/// Returns a JSON representation of the subtree rooted at the
/// [DiagnosticsNode] object that `diagnosticsNodeId` references providing
/// information needed for the details subtree view.
///
/// See also:
/// * [getChildrenDetailsSubtree], a method to get children of a node
/// in the details subtree.
String
getDetailsSubtree
(
String
id
,
String
groupName
)
{
return
_safeJsonEncode
(
_getDetailsSubtree
(
id
,
groupName
));
}
Map
<
String
,
Object
>
_getDetailsSubtree
(
String
id
,
String
groupName
)
{
final
DiagnosticsNode
root
=
toObject
(
id
);
if
(
root
==
null
)
{
return
null
;
}
return
_nodeToJson
(
root
,
new
_SerializeConfig
(
groupName:
groupName
,
summaryTree:
false
,
subtreeDepth:
2
,
// TODO(jacobr): make subtreeDepth configurable.
includeProperties:
true
,
),
);
}
}
/// Returns a [DiagnosticsNode] representing the currently selected
/// Returns a [DiagnosticsNode] representing the currently selected
...
@@ -713,13 +1017,13 @@ class WidgetInspectorService {
...
@@ -713,13 +1017,13 @@ class WidgetInspectorService {
/// [DiagnosticNode] is reused.
/// [DiagnosticNode] is reused.
@protected
@protected
String
getSelectedRenderObject
(
String
previousSelectionId
,
String
groupName
)
{
String
getSelectedRenderObject
(
String
previousSelectionId
,
String
groupName
)
{
return
json
.
e
ncode
(
_getSelectedRenderObject
(
previousSelectionId
,
groupName
));
return
_safeJsonE
ncode
(
_getSelectedRenderObject
(
previousSelectionId
,
groupName
));
}
}
Map
<
String
,
Object
>
_getSelectedRenderObject
(
String
previousSelectionId
,
String
groupName
)
{
Map
<
String
,
Object
>
_getSelectedRenderObject
(
String
previousSelectionId
,
String
groupName
)
{
final
DiagnosticsNode
previousSelection
=
toObject
(
previousSelectionId
);
final
DiagnosticsNode
previousSelection
=
toObject
(
previousSelectionId
);
final
RenderObject
current
=
selection
?.
current
;
final
RenderObject
current
=
selection
?.
current
;
return
_
serializeToJson
(
current
==
previousSelection
?.
value
?
previousSelection
:
current
?.
toDiagnosticsNode
(),
groupName
);
return
_
nodeToJson
(
current
==
previousSelection
?.
value
?
previousSelection
:
current
?.
toDiagnosticsNode
(),
new
_SerializeConfig
(
groupName:
groupName
)
);
}
}
/// Returns a [DiagnosticsNode] representing the currently selected [Element].
/// Returns a [DiagnosticsNode] representing the currently selected [Element].
...
@@ -729,13 +1033,44 @@ class WidgetInspectorService {
...
@@ -729,13 +1033,44 @@ class WidgetInspectorService {
/// reused.
/// reused.
@protected
@protected
String
getSelectedWidget
(
String
previousSelectionId
,
String
groupName
)
{
String
getSelectedWidget
(
String
previousSelectionId
,
String
groupName
)
{
return
json
.
e
ncode
(
_getSelectedWidget
(
previousSelectionId
,
groupName
));
return
_safeJsonE
ncode
(
_getSelectedWidget
(
previousSelectionId
,
groupName
));
}
}
Map
<
String
,
Object
>
_getSelectedWidget
(
String
previousSelectionId
,
String
groupName
)
{
Map
<
String
,
Object
>
_getSelectedWidget
(
String
previousSelectionId
,
String
groupName
)
{
final
DiagnosticsNode
previousSelection
=
toObject
(
previousSelectionId
);
final
DiagnosticsNode
previousSelection
=
toObject
(
previousSelectionId
);
final
Element
current
=
selection
?.
currentElement
;
final
Element
current
=
selection
?.
currentElement
;
return
_serializeToJson
(
current
==
previousSelection
?.
value
?
previousSelection
:
current
?.
toDiagnosticsNode
(),
groupName
);
return
_nodeToJson
(
current
==
previousSelection
?.
value
?
previousSelection
:
current
?.
toDiagnosticsNode
(),
new
_SerializeConfig
(
groupName:
groupName
));
}
/// Returns a [DiagnosticsNode] representing the currently selected [Element]
/// if the selected [Element] should be shown in the summary tree otherwise
/// returns the first ancestor of the selected [Element] shown in the summary
/// tree.
///
/// If the currently selected [Element] is identical to the [Element]
/// referenced by `previousSelectionId` then the previous [DiagnosticNode] is
/// reused.
String
getSelectedSummaryWidget
(
String
previousSelectionId
,
String
groupName
)
{
return
_safeJsonEncode
(
_getSelectedSummaryWidget
(
previousSelectionId
,
groupName
));
}
Map
<
String
,
Object
>
_getSelectedSummaryWidget
(
String
previousSelectionId
,
String
groupName
)
{
if
(!
isWidgetCreationTracked
())
{
return
_getSelectedWidget
(
previousSelectionId
,
groupName
);
}
final
DiagnosticsNode
previousSelection
=
toObject
(
previousSelectionId
);
Element
current
=
selection
?.
currentElement
;
if
(
current
!=
null
&&
!
_isValueCreatedByLocalProject
(
current
))
{
Element
firstLocal
;
for
(
Element
candidate
in
current
.
debugGetDiagnosticChain
())
{
if
(
_isValueCreatedByLocalProject
(
candidate
))
{
firstLocal
=
candidate
;
break
;
}
}
current
=
firstLocal
;
}
return
_nodeToJson
(
current
==
previousSelection
?.
value
?
previousSelection
:
current
?.
toDiagnosticsNode
(),
new
_SerializeConfig
(
groupName:
groupName
));
}
}
/// Returns whether [Widget] creation locations are available.
/// Returns whether [Widget] creation locations are available.
...
@@ -745,7 +1080,12 @@ class WidgetInspectorService {
...
@@ -745,7 +1080,12 @@ class WidgetInspectorService {
/// is required as injecting creation locations requires a
/// is required as injecting creation locations requires a
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
/// [Dart Kernel Transformer](https://github.com/dart-lang/sdk/wiki/Kernel-Documentation).
@protected
@protected
bool
isWidgetCreationTracked
()
=>
new
_WidgetForTypeTests
()
is
_HasCreationLocation
;
bool
isWidgetCreationTracked
()
{
_widgetCreationTracked
??=
new
_WidgetForTypeTests
()
is
_HasCreationLocation
;
return
_widgetCreationTracked
;
}
bool
_widgetCreationTracked
;
}
}
class
_WidgetForTypeTests
extends
Widget
{
class
_WidgetForTypeTests
extends
Widget
{
...
...
packages/flutter/test/foundation/service_extensions_test.dart
View file @
928c41e9
...
@@ -511,7 +511,7 @@ void main() {
...
@@ -511,7 +511,7 @@ void main() {
// If you add a service extension... TEST IT! :-)
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
// ...then increment this number.
expect
(
binding
.
extensions
.
length
,
3
2
);
expect
(
binding
.
extensions
.
length
,
3
7
);
expect
(
console
,
isEmpty
);
expect
(
console
,
isEmpty
);
debugPrint
=
debugPrintThrottled
;
debugPrint
=
debugPrintThrottled
;
...
...
packages/flutter/test/widgets/widget_inspector_test.dart
View file @
928c41e9
...
@@ -876,6 +876,218 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
...
@@ -876,6 +876,218 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
}
}
});
});
testWidgets
(
'ext.flutter.inspector.getChildrenDetailsSubtree'
,
(
WidgetTester
tester
)
async
{
const
String
group
=
'test-group'
;
await
tester
.
pumpWidget
(
new
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
new
Stack
(
children:
const
<
Widget
>[
const
Text
(
'a'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'b'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'c'
,
textDirection:
TextDirection
.
ltr
),
],
),
),
);
final
DiagnosticsNode
diagnostic
=
find
.
byType
(
Stack
).
evaluate
().
first
.
toDiagnosticsNode
();
final
String
id
=
service
.
toId
(
diagnostic
,
group
);
final
List
<
Object
>
childrenJson
=
await
service
.
testExtension
(
'getChildrenDetailsSubtree'
,
<
String
,
String
>{
'arg'
:
id
,
'objectGroup'
:
group
});
final
List
<
DiagnosticsNode
>
children
=
diagnostic
.
getChildren
();
expect
(
children
.
length
,
equals
(
3
));
expect
(
childrenJson
.
length
,
equals
(
children
.
length
));
for
(
int
i
=
0
;
i
<
childrenJson
.
length
;
++
i
)
{
final
Map
<
String
,
Object
>
childJson
=
childrenJson
[
i
];
expect
(
service
.
toObject
(
childJson
[
'valueId'
]),
equals
(
children
[
i
].
value
));
expect
(
service
.
toObject
(
childJson
[
'objectId'
]),
const
isInstanceOf
<
DiagnosticsNode
>());
final
List
<
Object
>
propertiesJson
=
childJson
[
'properties'
];
final
DiagnosticsNode
diagnosticsNode
=
service
.
toObject
(
childJson
[
'objectId'
]);
final
List
<
DiagnosticsNode
>
expectedProperties
=
diagnosticsNode
.
getProperties
();
for
(
Map
<
String
,
Object
>
propertyJson
in
propertiesJson
)
{
final
Object
property
=
service
.
toObject
(
propertyJson
[
'objectId'
]);
expect
(
property
,
const
isInstanceOf
<
DiagnosticsNode
>());
expect
(
expectedProperties
.
contains
(
property
),
isTrue
);
}
}
});
testWidgets
(
'WidgetInspectorService getDetailsSubtree'
,
(
WidgetTester
tester
)
async
{
const
String
group
=
'test-group'
;
await
tester
.
pumpWidget
(
new
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
new
Stack
(
children:
const
<
Widget
>[
const
Text
(
'a'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'b'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'c'
,
textDirection:
TextDirection
.
ltr
),
],
),
),
);
final
DiagnosticsNode
diagnostic
=
find
.
byType
(
Stack
).
evaluate
().
first
.
toDiagnosticsNode
();
final
String
id
=
service
.
toId
(
diagnostic
,
group
);
final
Map
<
String
,
Object
>
subtreeJson
=
await
service
.
testExtension
(
'getDetailsSubtree'
,
<
String
,
String
>{
'arg'
:
id
,
'objectGroup'
:
group
});
expect
(
subtreeJson
[
'objectId'
],
equals
(
id
));
final
List
<
Object
>
childrenJson
=
subtreeJson
[
'children'
];
final
List
<
DiagnosticsNode
>
children
=
diagnostic
.
getChildren
();
expect
(
children
.
length
,
equals
(
3
));
expect
(
childrenJson
.
length
,
equals
(
children
.
length
));
for
(
int
i
=
0
;
i
<
childrenJson
.
length
;
++
i
)
{
final
Map
<
String
,
Object
>
childJson
=
childrenJson
[
i
];
expect
(
service
.
toObject
(
childJson
[
'valueId'
]),
equals
(
children
[
i
].
value
));
expect
(
service
.
toObject
(
childJson
[
'objectId'
]),
const
isInstanceOf
<
DiagnosticsNode
>());
final
List
<
Object
>
propertiesJson
=
childJson
[
'properties'
];
final
DiagnosticsNode
diagnosticsNode
=
service
.
toObject
(
childJson
[
'objectId'
]);
final
List
<
DiagnosticsNode
>
expectedProperties
=
diagnosticsNode
.
getProperties
();
for
(
Map
<
String
,
Object
>
propertyJson
in
propertiesJson
)
{
final
Object
property
=
service
.
toObject
(
propertyJson
[
'objectId'
]);
expect
(
property
,
const
isInstanceOf
<
DiagnosticsNode
>());
expect
(
expectedProperties
.
contains
(
property
),
isTrue
);
}
}
});
testWidgets
(
'ext.flutter.inspector.getRootWidgetSummaryTree'
,
(
WidgetTester
tester
)
async
{
const
String
group
=
'test-group'
;
await
tester
.
pumpWidget
(
new
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
new
Stack
(
children:
const
<
Widget
>[
const
Text
(
'a'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'b'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'c'
,
textDirection:
TextDirection
.
ltr
),
],
),
),
);
final
Element
elementA
=
find
.
text
(
'a'
).
evaluate
().
first
;
service
.
disposeAllGroups
();
await
service
.
testExtension
(
'setPubRootDirectories'
,
<
String
,
String
>{});
service
.
setSelection
(
elementA
,
'my-group'
);
final
Map
<
String
,
Object
>
jsonA
=
await
service
.
testExtension
(
'getSelectedWidget'
,
<
String
,
String
>{
'arg'
:
null
,
'objectGroup'
:
'my-group'
});
await
service
.
testExtension
(
'setPubRootDirectories'
,
<
String
,
String
>{});
Map
<
String
,
Object
>
rootJson
=
await
service
.
testExtension
(
'getRootWidgetSummaryTree'
,
<
String
,
String
>{
'objectGroup'
:
group
});
// We haven't yet properly specified which directories are summary tree
// directories so we get an empty tree other than the root that is always
// included.
final
Object
rootWidget
=
service
.
toObject
(
rootJson
[
'valueId'
]);
expect
(
rootWidget
,
equals
(
WidgetsBinding
.
instance
?.
renderViewElement
));
List
<
Object
>
childrenJson
=
rootJson
[
'children'
];
// There are no summary tree children.
expect
(
childrenJson
.
length
,
equals
(
0
));
final
Map
<
String
,
Object
>
creationLocation
=
jsonA
[
'creationLocation'
];
expect
(
creationLocation
,
isNotNull
);
final
String
testFile
=
creationLocation
[
'file'
];
expect
(
testFile
,
endsWith
(
'widget_inspector_test.dart'
));
final
List
<
String
>
segments
=
Uri
.
parse
(
testFile
).
pathSegments
;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final
String
pubRootTest
=
'/'
+
segments
.
take
(
segments
.
length
-
2
).
join
(
'/'
);
await
service
.
testExtension
(
'setPubRootDirectories'
,
<
String
,
String
>{
'arg0'
:
pubRootTest
});
rootJson
=
await
service
.
testExtension
(
'getRootWidgetSummaryTree'
,
<
String
,
String
>{
'objectGroup'
:
group
});
childrenJson
=
rootJson
[
'children'
];
// The tree of nodes returned contains all widgets created directly by the
// test.
childrenJson
=
rootJson
[
'children'
];
expect
(
childrenJson
.
length
,
equals
(
1
));
List
<
Object
>
alternateChildrenJson
=
await
service
.
testExtension
(
'getChildrenSummaryTree'
,
<
String
,
String
>{
'arg'
:
rootJson
[
'objectId'
],
'objectGroup'
:
group
});
expect
(
alternateChildrenJson
.
length
,
equals
(
1
));
Map
<
String
,
Object
>
childJson
=
childrenJson
[
0
];
Map
<
String
,
Object
>
alternateChildJson
=
alternateChildrenJson
[
0
];
expect
(
childJson
[
'description'
],
startsWith
(
'Directionality'
));
expect
(
alternateChildJson
[
'description'
],
startsWith
(
'Directionality'
));
expect
(
alternateChildJson
[
'valueId'
],
equals
(
childJson
[
'valueId'
]));
childrenJson
=
childJson
[
'children'
];
alternateChildrenJson
=
await
service
.
testExtension
(
'getChildrenSummaryTree'
,
<
String
,
String
>{
'arg'
:
childJson
[
'objectId'
],
'objectGroup'
:
group
});
expect
(
alternateChildrenJson
.
length
,
equals
(
1
));
expect
(
childrenJson
.
length
,
equals
(
1
));
alternateChildJson
=
alternateChildrenJson
[
0
];
childJson
=
childrenJson
[
0
];
expect
(
childJson
[
'description'
],
startsWith
(
'Stack'
));
expect
(
alternateChildJson
[
'description'
],
startsWith
(
'Stack'
));
expect
(
alternateChildJson
[
'valueId'
],
equals
(
childJson
[
'valueId'
]));
childrenJson
=
childJson
[
'children'
];
childrenJson
=
childJson
[
'children'
];
alternateChildrenJson
=
await
service
.
testExtension
(
'getChildrenSummaryTree'
,
<
String
,
String
>{
'arg'
:
childJson
[
'objectId'
],
'objectGroup'
:
group
});
expect
(
alternateChildrenJson
.
length
,
equals
(
3
));
expect
(
childrenJson
.
length
,
equals
(
3
));
alternateChildJson
=
alternateChildrenJson
[
2
];
childJson
=
childrenJson
[
2
];
expect
(
childJson
[
'description'
],
startsWith
(
'Text'
));
expect
(
alternateChildJson
[
'description'
],
startsWith
(
'Text'
));
expect
(
alternateChildJson
[
'valueId'
],
equals
(
childJson
[
'valueId'
]));
alternateChildrenJson
=
await
service
.
testExtension
(
'getChildrenSummaryTree'
,
<
String
,
String
>{
'arg'
:
childJson
[
'objectId'
],
'objectGroup'
:
group
});
expect
(
alternateChildrenJson
.
length
,
equals
(
0
));
expect
(
childJson
[
'chidlren'
],
isNull
);
},
skip:
!
WidgetInspectorService
.
instance
.
isWidgetCreationTracked
());
// Test requires --track-widget-creation flag.
testWidgets
(
'ext.flutter.inspector.getSelectedSummaryWidget'
,
(
WidgetTester
tester
)
async
{
const
String
group
=
'test-group'
;
await
tester
.
pumpWidget
(
new
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
new
Stack
(
children:
const
<
Widget
>[
const
Text
(
'a'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'b'
,
textDirection:
TextDirection
.
ltr
),
const
Text
(
'c'
,
textDirection:
TextDirection
.
ltr
),
],
),
),
);
final
Element
elementA
=
find
.
text
(
'a'
).
evaluate
().
first
;
final
List
<
DiagnosticsNode
>
children
=
elementA
.
debugDescribeChildren
();
expect
(
children
.
length
,
equals
(
1
));
final
DiagnosticsNode
richTextDiagnostic
=
children
.
first
;
service
.
disposeAllGroups
();
await
service
.
testExtension
(
'setPubRootDirectories'
,
<
String
,
String
>{});
service
.
setSelection
(
elementA
,
'my-group'
);
final
Map
<
String
,
Object
>
jsonA
=
await
service
.
testExtension
(
'getSelectedWidget'
,
<
String
,
String
>{
'arg'
:
null
,
'objectGroup'
:
'my-group'
});
service
.
setSelection
(
richTextDiagnostic
.
value
,
'my-group'
);
await
service
.
testExtension
(
'setPubRootDirectories'
,
<
String
,
String
>{});
Map
<
String
,
Object
>
summarySelection
=
await
service
.
testExtension
(
'getSelectedSummaryWidget'
,
<
String
,
String
>{
'objectGroup'
:
group
});
// No summary selection because we haven't set the pub root directories
// yet to indicate what directories are in the summary tree.
expect
(
summarySelection
,
isNull
);
final
Map
<
String
,
Object
>
creationLocation
=
jsonA
[
'creationLocation'
];
expect
(
creationLocation
,
isNotNull
);
final
String
testFile
=
creationLocation
[
'file'
];
expect
(
testFile
,
endsWith
(
'widget_inspector_test.dart'
));
final
List
<
String
>
segments
=
Uri
.
parse
(
testFile
).
pathSegments
;
// Strip a couple subdirectories away to generate a plausible pub root
// directory.
final
String
pubRootTest
=
'/'
+
segments
.
take
(
segments
.
length
-
2
).
join
(
'/'
);
await
service
.
testExtension
(
'setPubRootDirectories'
,
<
String
,
String
>{
'arg0'
:
pubRootTest
});
summarySelection
=
await
service
.
testExtension
(
'getSelectedSummaryWidget'
,
<
String
,
String
>{
'objectGroup'
:
group
});
expect
(
summarySelection
[
'valueId'
],
isNotNull
);
// We got the Text element instead of the selected RichText element
// because only the RichText element is part of the summary tree.
expect
(
service
.
toObject
(
summarySelection
[
'valueId'
]),
elementA
);
// Verify tha the regular getSelectedWidget method still returns
// the RichText object not the Text element.
final
Map
<
String
,
Object
>
regularSelection
=
await
service
.
testExtension
(
'getSelectedWidget'
,
<
String
,
String
>{
'arg'
:
null
,
'objectGroup'
:
'my-group'
});
expect
(
service
.
toObject
(
regularSelection
[
'valueId'
]),
richTextDiagnostic
.
value
);
},
skip:
!
WidgetInspectorService
.
instance
.
isWidgetCreationTracked
());
// Test requires --track-widget-creation flag.
testWidgets
(
'ext.flutter.inspector creationLocation'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'ext.flutter.inspector creationLocation'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
await
tester
.
pumpWidget
(
new
Directionality
(
new
Directionality
(
...
...
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