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
d2d17abe
Unverified
Commit
d2d17abe
authored
Jul 20, 2018
by
Jonah Williams
Committed by
GitHub
Jul 20, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add support for custom semantics actions to Android and iOS. (#18882)
parent
924c206c
Changes
6
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
401 additions
and
57 deletions
+401
-57
leave_behind_demo.dart
.../flutter_gallery/lib/demo/material/leave_behind_demo.dart
+113
-51
proxy_box.dart
packages/flutter/lib/src/rendering/proxy_box.dart
+22
-0
semantics.dart
packages/flutter/lib/src/semantics/semantics.dart
+190
-5
basic.dart
packages/flutter/lib/src/widgets/basic.dart
+5
-1
custom_semantics_action_test.dart
.../flutter/test/semantics/custom_semantics_action_test.dart
+25
-0
semantics_test.dart
packages/flutter/test/semantics/semantics_test.dart
+46
-0
No files found.
examples/flutter_gallery/lib/demo/material/leave_behind_demo.dart
View file @
d2d17abe
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
import
'package:collection/collection.dart'
show
lowerBound
;
import
'package:collection/collection.dart'
show
lowerBound
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/semantics.dart'
;
enum
LeaveBehindDemoAction
{
enum
LeaveBehindDemoAction
{
reset
,
reset
,
...
@@ -85,52 +86,55 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
...
@@ -85,52 +86,55 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
});
});
}
}
Widget
buildItem
(
LeaveBehindItem
item
)
{
void
_handleArchive
(
LeaveBehindItem
item
)
{
final
ThemeData
theme
=
Theme
.
of
(
context
);
setState
(()
{
return
new
Dismissible
(
leaveBehindItems
.
remove
(
item
);
key:
new
ObjectKey
(
item
),
});
direction:
_dismissDirection
,
_scaffoldKey
.
currentState
.
showSnackBar
(
new
SnackBar
(
onDismissed:
(
DismissDirection
direction
)
{
content:
new
Text
(
'You archived item
${item.index}
'
),
setState
(()
{
action:
new
SnackBarAction
(
leaveBehindItems
.
remove
(
item
);
label:
'UNDO'
,
});
onPressed:
()
{
handleUndo
(
item
);
}
final
String
action
=
(
direction
==
DismissDirection
.
endToStart
)
?
'archived'
:
'deleted'
;
_scaffoldKey
.
currentState
.
showSnackBar
(
new
SnackBar
(
content:
new
Text
(
'You
$action
item
${item.index}
'
),
action:
new
SnackBarAction
(
label:
'UNDO'
,
onPressed:
()
{
handleUndo
(
item
);
}
)
));
},
background:
new
Container
(
color:
theme
.
primaryColor
,
child:
const
ListTile
(
leading:
const
Icon
(
Icons
.
delete
,
color:
Colors
.
white
,
size:
36.0
)
)
),
secondaryBackground:
new
Container
(
color:
theme
.
primaryColor
,
child:
const
ListTile
(
trailing:
const
Icon
(
Icons
.
archive
,
color:
Colors
.
white
,
size:
36.0
)
)
),
child:
new
Container
(
decoration:
new
BoxDecoration
(
color:
theme
.
canvasColor
,
border:
new
Border
(
bottom:
new
BorderSide
(
color:
theme
.
dividerColor
))
),
child:
new
ListTile
(
title:
new
Text
(
item
.
name
),
subtitle:
new
Text
(
'
${item.subject}
\n
${item.body}
'
),
isThreeLine:
true
)
)
)
);
));
}
void
_handleDelete
(
LeaveBehindItem
item
)
{
setState
(()
{
leaveBehindItems
.
remove
(
item
);
});
_scaffoldKey
.
currentState
.
showSnackBar
(
new
SnackBar
(
content:
new
Text
(
'You deleted item
${item.index}
'
),
action:
new
SnackBarAction
(
label:
'UNDO'
,
onPressed:
()
{
handleUndo
(
item
);
}
)
));
}
}
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
Widget
body
;
if
(
leaveBehindItems
.
isEmpty
)
{
body
=
new
Center
(
child:
new
RaisedButton
(
onPressed:
()
=>
handleDemoAction
(
LeaveBehindDemoAction
.
reset
),
child:
const
Text
(
'Reset the list'
),
),
);
}
else
{
body
=
new
ListView
(
children:
leaveBehindItems
.
map
((
LeaveBehindItem
item
)
{
return
new
_LeaveBehindListItem
(
item:
item
,
onArchive:
_handleArchive
,
onDelete:
_handleDelete
,
dismissDirection:
_dismissDirection
,
);
}).
toList
()
);
}
return
new
Scaffold
(
return
new
Scaffold
(
key:
_scaffoldKey
,
key:
_scaffoldKey
,
appBar:
new
AppBar
(
appBar:
new
AppBar
(
...
@@ -163,16 +167,74 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
...
@@ -163,16 +167,74 @@ class LeaveBehindDemoState extends State<LeaveBehindDemo> {
)
)
]
]
),
),
body:
leaveBehindItems
.
isEmpty
body:
body
,
?
new
Center
(
);
child:
new
RaisedButton
(
}
onPressed:
()
=>
handleDemoAction
(
LeaveBehindDemoAction
.
reset
),
}
child:
const
Text
(
'Reset the list'
),
),
class
_LeaveBehindListItem
extends
StatelessWidget
{
)
const
_LeaveBehindListItem
({
:
new
ListView
(
Key
key
,
children:
leaveBehindItems
.
map
(
buildItem
).
toList
()
@required
this
.
item
,
),
@required
this
.
onArchive
,
@required
this
.
onDelete
,
@required
this
.
dismissDirection
,
})
:
super
(
key:
key
);
final
LeaveBehindItem
item
;
final
DismissDirection
dismissDirection
;
final
void
Function
(
LeaveBehindItem
)
onArchive
;
final
void
Function
(
LeaveBehindItem
)
onDelete
;
void
_handleArchive
()
{
onArchive
(
item
);
}
void
_handleDelete
()
{
onDelete
(
item
);
}
@override
Widget
build
(
BuildContext
context
)
{
final
ThemeData
theme
=
Theme
.
of
(
context
);
return
new
Semantics
(
customSemanticsActions:
<
CustomSemanticsAction
,
VoidCallback
>{
const
CustomSemanticsAction
(
label:
'Archive'
):
_handleArchive
,
const
CustomSemanticsAction
(
label:
'Delete'
):
_handleDelete
,
},
child:
new
Dismissible
(
key:
new
ObjectKey
(
item
),
direction:
dismissDirection
,
onDismissed:
(
DismissDirection
direction
)
{
if
(
direction
==
DismissDirection
.
endToStart
)
_handleArchive
();
else
_handleDelete
();
},
background:
new
Container
(
color:
theme
.
primaryColor
,
child:
const
ListTile
(
leading:
const
Icon
(
Icons
.
delete
,
color:
Colors
.
white
,
size:
36.0
)
)
),
secondaryBackground:
new
Container
(
color:
theme
.
primaryColor
,
child:
const
ListTile
(
trailing:
const
Icon
(
Icons
.
archive
,
color:
Colors
.
white
,
size:
36.0
)
)
),
child:
new
Container
(
decoration:
new
BoxDecoration
(
color:
theme
.
canvasColor
,
border:
new
Border
(
bottom:
new
BorderSide
(
color:
theme
.
dividerColor
))
),
child:
new
ListTile
(
title:
new
Text
(
item
.
name
),
subtitle:
new
Text
(
'
${item.subject}
\n
${item.body}
'
),
isThreeLine:
true
),
),
),
);
);
}
}
}
}
packages/flutter/lib/src/rendering/proxy_box.dart
View file @
d2d17abe
...
@@ -3159,6 +3159,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
...
@@ -3159,6 +3159,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
SetSelectionHandler
onSetSelection
,
SetSelectionHandler
onSetSelection
,
VoidCallback
onDidGainAccessibilityFocus
,
VoidCallback
onDidGainAccessibilityFocus
,
VoidCallback
onDidLoseAccessibilityFocus
,
VoidCallback
onDidLoseAccessibilityFocus
,
Map
<
CustomSemanticsAction
,
VoidCallback
>
customSemanticsActions
,
})
:
assert
(
container
!=
null
),
})
:
assert
(
container
!=
null
),
_container
=
container
,
_container
=
container
,
_explicitChildNodes
=
explicitChildNodes
,
_explicitChildNodes
=
explicitChildNodes
,
...
@@ -3197,6 +3198,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
...
@@ -3197,6 +3198,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_onSetSelection
=
onSetSelection
,
_onSetSelection
=
onSetSelection
,
_onDidGainAccessibilityFocus
=
onDidGainAccessibilityFocus
,
_onDidGainAccessibilityFocus
=
onDidGainAccessibilityFocus
,
_onDidLoseAccessibilityFocus
=
onDidLoseAccessibilityFocus
,
_onDidLoseAccessibilityFocus
=
onDidLoseAccessibilityFocus
,
_customSemanticsActions
=
customSemanticsActions
,
super
(
child
);
super
(
child
);
/// If 'container' is true, this [RenderObject] will introduce a new
/// If 'container' is true, this [RenderObject] will introduce a new
...
@@ -3779,6 +3781,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
...
@@ -3779,6 +3781,24 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate
();
markNeedsSemanticsUpdate
();
}
}
/// The handlers and supported [CustomSemanticsAction]s for this node.
///
/// These handlers are called whenever the user performs the associated
/// custom accessibility action from a special platform menu. Providing any
/// custom actions here also adds [SemanticsAction.customAction] to the node.
///
/// See also:
///
/// * [CustomSemanticsAction], for an explaination of custom actions.
Map
<
CustomSemanticsAction
,
VoidCallback
>
get
customSemanticsActions
=>
_customSemanticsActions
;
Map
<
CustomSemanticsAction
,
VoidCallback
>
_customSemanticsActions
;
set
customSemanticsActions
(
Map
<
CustomSemanticsAction
,
VoidCallback
>
value
)
{
if
(
_customSemanticsActions
==
value
)
return
;
_customSemanticsActions
=
value
;
markNeedsSemanticsUpdate
();
}
@override
@override
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
void
describeSemanticsConfiguration
(
SemanticsConfiguration
config
)
{
super
.
describeSemanticsConfiguration
(
config
);
super
.
describeSemanticsConfiguration
(
config
);
...
@@ -3860,6 +3880,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
...
@@ -3860,6 +3880,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config
.
onDidGainAccessibilityFocus
=
_performDidGainAccessibilityFocus
;
config
.
onDidGainAccessibilityFocus
=
_performDidGainAccessibilityFocus
;
if
(
onDidLoseAccessibilityFocus
!=
null
)
if
(
onDidLoseAccessibilityFocus
!=
null
)
config
.
onDidLoseAccessibilityFocus
=
_performDidLoseAccessibilityFocus
;
config
.
onDidLoseAccessibilityFocus
=
_performDidLoseAccessibilityFocus
;
if
(
customSemanticsActions
!=
null
)
config
.
customSemanticsActions
=
_customSemanticsActions
;
}
}
void
_performTap
()
{
void
_performTap
()
{
...
...
packages/flutter/lib/src/semantics/semantics.dart
View file @
d2d17abe
This diff is collapsed.
Click to expand it.
packages/flutter/lib/src/widgets/basic.dart
View file @
d2d17abe
...
@@ -5089,6 +5089,7 @@ class Semantics extends SingleChildRenderObjectWidget {
...
@@ -5089,6 +5089,7 @@ class Semantics extends SingleChildRenderObjectWidget {
SetSelectionHandler
onSetSelection
,
SetSelectionHandler
onSetSelection
,
VoidCallback
onDidGainAccessibilityFocus
,
VoidCallback
onDidGainAccessibilityFocus
,
VoidCallback
onDidLoseAccessibilityFocus
,
VoidCallback
onDidLoseAccessibilityFocus
,
Map
<
CustomSemanticsAction
,
VoidCallback
>
customSemanticsActions
,
})
:
this
.
fromProperties
(
})
:
this
.
fromProperties
(
key:
key
,
key:
key
,
child:
child
,
child:
child
,
...
@@ -5129,6 +5130,7 @@ class Semantics extends SingleChildRenderObjectWidget {
...
@@ -5129,6 +5130,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onMoveCursorBackwardByCharacter:
onMoveCursorBackwardByCharacter
,
onMoveCursorBackwardByCharacter:
onMoveCursorBackwardByCharacter
,
onDidGainAccessibilityFocus:
onDidGainAccessibilityFocus
,
onDidGainAccessibilityFocus:
onDidGainAccessibilityFocus
,
onDidLoseAccessibilityFocus:
onDidLoseAccessibilityFocus
,
onDidLoseAccessibilityFocus:
onDidLoseAccessibilityFocus
,
customSemanticsActions:
customSemanticsActions
,
onSetSelection:
onSetSelection
,),
onSetSelection:
onSetSelection
,),
);
);
...
@@ -5216,6 +5218,7 @@ class Semantics extends SingleChildRenderObjectWidget {
...
@@ -5216,6 +5218,7 @@ class Semantics extends SingleChildRenderObjectWidget {
onSetSelection:
properties
.
onSetSelection
,
onSetSelection:
properties
.
onSetSelection
,
onDidGainAccessibilityFocus:
properties
.
onDidGainAccessibilityFocus
,
onDidGainAccessibilityFocus:
properties
.
onDidGainAccessibilityFocus
,
onDidLoseAccessibilityFocus:
properties
.
onDidLoseAccessibilityFocus
,
onDidLoseAccessibilityFocus:
properties
.
onDidLoseAccessibilityFocus
,
customSemanticsActions:
properties
.
customSemanticsActions
,
);
);
}
}
...
@@ -5270,7 +5273,8 @@ class Semantics extends SingleChildRenderObjectWidget {
...
@@ -5270,7 +5273,8 @@ class Semantics extends SingleChildRenderObjectWidget {
..
onMoveCursorBackwardByCharacter
=
properties
.
onMoveCursorForwardByCharacter
..
onMoveCursorBackwardByCharacter
=
properties
.
onMoveCursorForwardByCharacter
..
onSetSelection
=
properties
.
onSetSelection
..
onSetSelection
=
properties
.
onSetSelection
..
onDidGainAccessibilityFocus
=
properties
.
onDidGainAccessibilityFocus
..
onDidGainAccessibilityFocus
=
properties
.
onDidGainAccessibilityFocus
..
onDidLoseAccessibilityFocus
=
properties
.
onDidLoseAccessibilityFocus
;
..
onDidLoseAccessibilityFocus
=
properties
.
onDidLoseAccessibilityFocus
..
customSemanticsActions
=
properties
.
customSemanticsActions
;
}
}
@override
@override
...
...
packages/flutter/test/semantics/custom_semantics_action_test.dart
0 → 100644
View file @
d2d17abe
import
'package:test/test.dart'
;
import
'package:flutter/semantics.dart'
;
void
main
(
)
{
group
(
CustomSemanticsAction
,
()
{
test
(
'is provided a canonical id based on the label'
,
()
{
final
CustomSemanticsAction
action1
=
new
CustomSemanticsAction
(
label:
_nonconst
(
'test'
));
final
CustomSemanticsAction
action2
=
new
CustomSemanticsAction
(
label:
_nonconst
(
'test'
));
final
CustomSemanticsAction
action3
=
new
CustomSemanticsAction
(
label:
_nonconst
(
'not test'
));
final
int
id1
=
CustomSemanticsAction
.
getIdentifier
(
action1
);
final
int
id2
=
CustomSemanticsAction
.
getIdentifier
(
action2
);
final
int
id3
=
CustomSemanticsAction
.
getIdentifier
(
action3
);
expect
(
id1
,
id2
);
expect
(
id2
,
isNot
(
id3
));
expect
(
CustomSemanticsAction
.
getAction
(
id1
),
action1
);
expect
(
CustomSemanticsAction
.
getAction
(
id2
),
action1
);
expect
(
CustomSemanticsAction
.
getAction
(
id3
),
action3
);
});
});
}
T
_nonconst
<
T
>(
T
value
)
=>
value
;
packages/flutter/test/semantics/semantics_test.dart
View file @
d2d17abe
...
@@ -337,6 +337,7 @@ void main() {
...
@@ -337,6 +337,7 @@ void main() {
' mergeAllDescendantsIntoThisNode: false
\n
'
' mergeAllDescendantsIntoThisNode: false
\n
'
' Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)
\n
'
' Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)
\n
'
' actions: []
\n
'
' actions: []
\n
'
' customActions: []
\n
'
' flags: []
\n
'
' flags: []
\n
'
' invisible
\n
'
' invisible
\n
'
' isHidden: false
\n
'
' isHidden: false
\n
'
...
@@ -404,8 +405,49 @@ void main() {
...
@@ -404,8 +405,49 @@ void main() {
);
);
});
});
test
(
'Custom actions debug properties'
,
()
{
final
SemanticsConfiguration
configuration
=
new
SemanticsConfiguration
();
const
CustomSemanticsAction
action1
=
const
CustomSemanticsAction
(
label:
'action1'
);
const
CustomSemanticsAction
action2
=
const
CustomSemanticsAction
(
label:
'action2'
);
const
CustomSemanticsAction
action3
=
const
CustomSemanticsAction
(
label:
'action3'
);
configuration
.
customSemanticsActions
=
<
CustomSemanticsAction
,
VoidCallback
>{
action1:
()
{},
action2:
()
{},
action3:
()
{},
};
final
SemanticsNode
actionNode
=
new
SemanticsNode
();
actionNode
.
updateWith
(
config:
configuration
);
expect
(
actionNode
.
toStringDeep
(
minLevel:
DiagnosticLevel
.
hidden
),
'SemanticsNode#1
\n
'
' STALE
\n
'
' owner: null
\n
'
' isMergedIntoParent: false
\n
'
' mergeAllDescendantsIntoThisNode: false
\n
'
' Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)
\n
'
' actions: customAction
\n
'
' customActions: action1, action2, action3
\n
'
' flags: []
\n
'
' invisible
\n
'
' isHidden: false
\n
'
' label: ""
\n
'
' value: ""
\n
'
' increasedValue: ""
\n
'
' decreasedValue: ""
\n
'
' hint: ""
\n
'
' textDirection: null
\n
'
' sortKey: null
\n
'
' scrollExtentMin: null
\n
'
' scrollPosition: null
\n
'
' scrollExtentMax: null
\n
'
);
});
test
(
'SemanticsConfiguration getter/setter'
,
()
{
test
(
'SemanticsConfiguration getter/setter'
,
()
{
final
SemanticsConfiguration
config
=
new
SemanticsConfiguration
();
final
SemanticsConfiguration
config
=
new
SemanticsConfiguration
();
const
CustomSemanticsAction
customAction
=
const
CustomSemanticsAction
(
label:
'test'
);
expect
(
config
.
isSemanticBoundary
,
isFalse
);
expect
(
config
.
isSemanticBoundary
,
isFalse
);
expect
(
config
.
isButton
,
isFalse
);
expect
(
config
.
isButton
,
isFalse
);
...
@@ -428,6 +470,7 @@ void main() {
...
@@ -428,6 +470,7 @@ void main() {
expect
(
config
.
onMoveCursorForwardByCharacter
,
isNull
);
expect
(
config
.
onMoveCursorForwardByCharacter
,
isNull
);
expect
(
config
.
onMoveCursorBackwardByCharacter
,
isNull
);
expect
(
config
.
onMoveCursorBackwardByCharacter
,
isNull
);
expect
(
config
.
onTap
,
isNull
);
expect
(
config
.
onTap
,
isNull
);
expect
(
config
.
customSemanticsActions
[
customAction
],
isNull
);
config
.
isSemanticBoundary
=
true
;
config
.
isSemanticBoundary
=
true
;
config
.
isButton
=
true
;
config
.
isButton
=
true
;
...
@@ -450,6 +493,7 @@ void main() {
...
@@ -450,6 +493,7 @@ void main() {
final
MoveCursorHandler
onMoveCursorForwardByCharacter
=
(
bool
_
)
{
};
final
MoveCursorHandler
onMoveCursorForwardByCharacter
=
(
bool
_
)
{
};
final
MoveCursorHandler
onMoveCursorBackwardByCharacter
=
(
bool
_
)
{
};
final
MoveCursorHandler
onMoveCursorBackwardByCharacter
=
(
bool
_
)
{
};
final
VoidCallback
onTap
=
()
{
};
final
VoidCallback
onTap
=
()
{
};
final
VoidCallback
onCustomAction
=
()
{};
config
.
onShowOnScreen
=
onShowOnScreen
;
config
.
onShowOnScreen
=
onShowOnScreen
;
config
.
onScrollDown
=
onScrollDown
;
config
.
onScrollDown
=
onScrollDown
;
...
@@ -462,6 +506,7 @@ void main() {
...
@@ -462,6 +506,7 @@ void main() {
config
.
onMoveCursorForwardByCharacter
=
onMoveCursorForwardByCharacter
;
config
.
onMoveCursorForwardByCharacter
=
onMoveCursorForwardByCharacter
;
config
.
onMoveCursorBackwardByCharacter
=
onMoveCursorBackwardByCharacter
;
config
.
onMoveCursorBackwardByCharacter
=
onMoveCursorBackwardByCharacter
;
config
.
onTap
=
onTap
;
config
.
onTap
=
onTap
;
config
.
customSemanticsActions
[
customAction
]
=
onCustomAction
;
expect
(
config
.
isSemanticBoundary
,
isTrue
);
expect
(
config
.
isSemanticBoundary
,
isTrue
);
expect
(
config
.
isButton
,
isTrue
);
expect
(
config
.
isButton
,
isTrue
);
...
@@ -484,6 +529,7 @@ void main() {
...
@@ -484,6 +529,7 @@ void main() {
expect
(
config
.
onMoveCursorForwardByCharacter
,
same
(
onMoveCursorForwardByCharacter
));
expect
(
config
.
onMoveCursorForwardByCharacter
,
same
(
onMoveCursorForwardByCharacter
));
expect
(
config
.
onMoveCursorBackwardByCharacter
,
same
(
onMoveCursorBackwardByCharacter
));
expect
(
config
.
onMoveCursorBackwardByCharacter
,
same
(
onMoveCursorBackwardByCharacter
));
expect
(
config
.
onTap
,
same
(
onTap
));
expect
(
config
.
onTap
,
same
(
onTap
));
expect
(
config
.
customSemanticsActions
[
customAction
],
same
(
onCustomAction
));
});
});
}
}
...
...
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