Commit 6b79d377 authored by escamoteur's avatar escamoteur Committed by Michael Klimushyn

Add `disabledHint` to DropdownButton (#18770)

If `items` or `onChanged` is null the button will be disabled, the down arrow will be grayed out, and
the new `disabledHint` will be shown (if provided).
parent 707eaf5e
...@@ -493,6 +493,8 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -493,6 +493,8 @@ class DropdownButton<T> extends StatefulWidget {
/// Creates a dropdown button. /// Creates a dropdown button.
/// ///
/// The [items] must have distinct values and if [value] isn't null it must be among them. /// The [items] must have distinct values and if [value] isn't null it must be among them.
/// If [items] or [onChanged] is null, the button will be disabled, the down arrow will be grayed out, and
/// the [disabledHint] will be shown (if provided).
/// ///
/// The [elevation] and [iconSize] arguments must not be null (they both have /// The [elevation] and [iconSize] arguments must not be null (they both have
/// defaults, so do not need to be specified). /// defaults, so do not need to be specified).
...@@ -501,14 +503,14 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -501,14 +503,14 @@ class DropdownButton<T> extends StatefulWidget {
@required this.items, @required this.items,
this.value, this.value,
this.hint, this.hint,
this.disabledHint,
@required this.onChanged, @required this.onChanged,
this.elevation = 8, this.elevation = 8,
this.style, this.style,
this.iconSize = 24.0, this.iconSize = 24.0,
this.isDense = false, this.isDense = false,
this.isExpanded = false, this.isExpanded = false,
}) : assert(items != null), }) : assert(items == null || value == null || items.where((DropdownMenuItem<T> item) => item.value == value).length == 1),
assert(value == null || items.where((DropdownMenuItem<T> item) => item.value == value).length == 1),
super(key: key); super(key: key);
/// The list of possible items to select among. /// The list of possible items to select among.
...@@ -522,6 +524,11 @@ class DropdownButton<T> extends StatefulWidget { ...@@ -522,6 +524,11 @@ class DropdownButton<T> extends StatefulWidget {
/// Displayed if [value] is null. /// Displayed if [value] is null.
final Widget hint; final Widget hint;
/// A message to show when the dropdown is disabled.
///
/// Displayed if [items] or [onChanged] is null.
final Widget disabledHint;
/// Called when the user selects an item. /// Called when the user selects an item.
final ValueChanged<T> onChanged; final ValueChanged<T> onChanged;
...@@ -600,6 +607,10 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -600,6 +607,10 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
} }
void _updateSelectedIndex() { void _updateSelectedIndex() {
if (!_enabled) {
return;
}
assert(widget.value == null || assert(widget.value == null ||
widget.items.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1); widget.items.where((DropdownMenuItem<T> item) => item.value == widget.value).length == 1);
_selectedIndex = null; _selectedIndex = null;
...@@ -650,6 +661,25 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -650,6 +661,25 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
return math.max(_textStyle.fontSize, math.max(widget.iconSize, _kDenseButtonHeight)); return math.max(_textStyle.fontSize, math.max(widget.iconSize, _kDenseButtonHeight));
} }
Color get _downArrowColor {
// These colors are not defined in the Material Design spec.
if (_enabled) {
if (Theme.of(context).brightness == Brightness.light) {
return Colors.grey.shade700;
} else {
return Colors.white70;
}
} else {
if (Theme.of(context).brightness == Brightness.light) {
return Colors.grey.shade400;
} else {
return Colors.white10;
}
}
}
bool get _enabled => widget.items != null && widget.items.isNotEmpty && widget.onChanged != null;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context)); assert(debugCheckHasMaterial(context));
...@@ -657,14 +687,16 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -657,14 +687,16 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
// The width of the button and the menu are defined by the widest // The width of the button and the menu are defined by the widest
// item and the width of the hint. // item and the width of the hint.
final List<Widget> items = List<Widget>.from(widget.items); final List<Widget> items = _enabled ? List<Widget>.from(widget.items) : <Widget>[];
int hintIndex; int hintIndex;
if (widget.hint != null) { if (widget.hint != null || (!_enabled && widget.disabledHint != null)) {
final Widget emplacedHint =
_enabled ? widget.hint : DropdownMenuItem<Widget>(child: widget.disabledHint ?? widget.hint);
hintIndex = items.length; hintIndex = items.length;
items.add(DefaultTextStyle( items.add(DefaultTextStyle(
style: _textStyle.copyWith(color: Theme.of(context).hintColor), style: _textStyle.copyWith(color: Theme.of(context).hintColor),
child: IgnorePointer( child: IgnorePointer(
child: widget.hint, child: emplacedHint,
ignoringSemantics: false, ignoringSemantics: false,
), ),
)); ));
...@@ -674,10 +706,10 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -674,10 +706,10 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
? _kAlignedButtonPadding ? _kAlignedButtonPadding
: _kUnalignedButtonPadding; : _kUnalignedButtonPadding;
// If value is null (then _selectedIndex is null) then we display // If value is null (then _selectedIndex is null) or if disabled then we
// the hint or nothing at all. // display the hint or nothing at all.
final IndexedStack innerItemsWidget = IndexedStack( final IndexedStack innerItemsWidget = IndexedStack(
index: _selectedIndex ?? hintIndex, index: _enabled ? (_selectedIndex ?? hintIndex) : hintIndex,
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
children: items, children: items,
); );
...@@ -694,8 +726,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -694,8 +726,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
widget.isExpanded ? Expanded(child: innerItemsWidget) : innerItemsWidget, widget.isExpanded ? Expanded(child: innerItemsWidget) : innerItemsWidget,
Icon(Icons.arrow_drop_down, Icon(Icons.arrow_drop_down,
size: widget.iconSize, size: widget.iconSize,
// These colors are not defined in the Material Design spec. color: _downArrowColor,
color: Theme.of(context).brightness == Brightness.light ? Colors.grey.shade700 : Colors.white70
), ),
], ],
), ),
...@@ -725,7 +756,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi ...@@ -725,7 +756,7 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
return Semantics( return Semantics(
button: true, button: true,
child: GestureDetector( child: GestureDetector(
onTap: _handleTap, onTap: _enabled ? _handleTap : null,
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: result child: result
), ),
......
...@@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart'; ...@@ -13,6 +13,7 @@ import 'package:flutter/rendering.dart';
import '../widgets/semantics_tester.dart'; import '../widgets/semantics_tester.dart';
const List<String> menuItems = <String>['one', 'two', 'three', 'four']; const List<String> menuItems = <String>['one', 'two', 'three', 'four'];
final ValueChanged<String> onChanged = (_) {};
final Type dropdownButtonType = DropdownButton<String>( final Type dropdownButtonType = DropdownButton<String>(
onChanged: (_) { }, onChanged: (_) { },
...@@ -26,6 +27,7 @@ Widget buildFrame({ ...@@ -26,6 +27,7 @@ Widget buildFrame({
bool isDense = false, bool isDense = false,
bool isExpanded = false, bool isExpanded = false,
Widget hint, Widget hint,
Widget disabledHint,
List<String> items = menuItems, List<String> items = menuItems,
Alignment alignment = Alignment.center, Alignment alignment = Alignment.center,
TextDirection textDirection = TextDirection.ltr, TextDirection textDirection = TextDirection.ltr,
...@@ -40,10 +42,11 @@ Widget buildFrame({ ...@@ -40,10 +42,11 @@ Widget buildFrame({
key: buttonKey, key: buttonKey,
value: value, value: value,
hint: hint, hint: hint,
disabledHint: disabledHint,
onChanged: onChanged, onChanged: onChanged,
isDense: isDense, isDense: isDense,
isExpanded: isExpanded, isExpanded: isExpanded,
items: items.map<DropdownMenuItem<String>>((String item) { items: items == null ? null : items.map<DropdownMenuItem<String>>((String item) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
key: ValueKey<String>(item), key: ValueKey<String>(item),
value: item, value: item,
...@@ -115,7 +118,7 @@ bool sameGeometry(RenderBox box1, RenderBox box2) { ...@@ -115,7 +118,7 @@ bool sameGeometry(RenderBox box1, RenderBox box2) {
void main() { void main() {
testWidgets('Default dropdown golden', (WidgetTester tester) async { testWidgets('Default dropdown golden', (WidgetTester tester) async {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
Widget build() => buildFrame(buttonKey: buttonKey, value: 'two'); Widget build() => buildFrame(buttonKey: buttonKey, value: 'two', onChanged: onChanged);
await tester.pumpWidget(build()); await tester.pumpWidget(build());
final Finder buttonFinder = find.byKey(buttonKey); final Finder buttonFinder = find.byKey(buttonKey);
assert(tester.renderObject(buttonFinder).attached); assert(tester.renderObject(buttonFinder).attached);
...@@ -128,7 +131,7 @@ void main() { ...@@ -128,7 +131,7 @@ void main() {
testWidgets('Expanded dropdown golden', (WidgetTester tester) async { testWidgets('Expanded dropdown golden', (WidgetTester tester) async {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
Widget build() => buildFrame(buttonKey: buttonKey, value: 'two', isExpanded: true); Widget build() => buildFrame(buttonKey: buttonKey, value: 'two', isExpanded: true, onChanged: onChanged);
await tester.pumpWidget(build()); await tester.pumpWidget(build());
final Finder buttonFinder = find.byKey(buttonKey); final Finder buttonFinder = find.byKey(buttonKey);
assert(tester.renderObject(buttonFinder).attached); assert(tester.renderObject(buttonFinder).attached);
...@@ -326,7 +329,7 @@ void main() { ...@@ -326,7 +329,7 @@ void main() {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
const String value = 'two'; const String value = 'two';
Widget build() => buildFrame(buttonKey: buttonKey, value: value, textDirection: textDirection); Widget build() => buildFrame(buttonKey: buttonKey, value: value, textDirection: textDirection, onChanged: onChanged);
await tester.pumpWidget(build()); await tester.pumpWidget(build());
final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey)); final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
...@@ -371,7 +374,7 @@ void main() { ...@@ -371,7 +374,7 @@ void main() {
testWidgets('Arrow icon aligns with the edge of button when expanded', (WidgetTester tester) async { testWidgets('Arrow icon aligns with the edge of button when expanded', (WidgetTester tester) async {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
Widget build() => buildFrame(buttonKey: buttonKey, value: 'two', isExpanded: true); Widget build() => buildFrame(buttonKey: buttonKey, value: 'two', isExpanded: true, onChanged: onChanged);
await tester.pumpWidget(build()); await tester.pumpWidget(build());
final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey)); final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
...@@ -389,7 +392,7 @@ void main() { ...@@ -389,7 +392,7 @@ void main() {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
const String value = 'two'; const String value = 'two';
Widget build() => buildFrame(buttonKey: buttonKey, value: value, isDense: true); Widget build() => buildFrame(buttonKey: buttonKey, value: value, isDense: true, onChanged: onChanged);
await tester.pumpWidget(build()); await tester.pumpWidget(build());
final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey)); final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
...@@ -428,7 +431,8 @@ void main() { ...@@ -428,7 +431,8 @@ void main() {
await tester.pumpWidget(buildFrame( await tester.pumpWidget(buildFrame(
buttonKey: buttonKey, buttonKey: buttonKey,
value: null, // nothing selected value: null, // nothing selected
items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()) items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()),
onChanged: onChanged,
)); ));
await tester.tap(find.byKey(buttonKey)); await tester.tap(find.byKey(buttonKey));
await tester.pump(); await tester.pump();
...@@ -455,7 +459,8 @@ void main() { ...@@ -455,7 +459,8 @@ void main() {
await tester.pumpWidget(buildFrame( await tester.pumpWidget(buildFrame(
buttonKey: buttonKey, buttonKey: buttonKey,
value: '50', value: '50',
items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()) items: List<String>.generate(/*length=*/ 100, (int index) => index.toString()),
onChanged: onChanged,
)); ));
final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey)); final RenderBox buttonBox = tester.renderObject(find.byKey(buttonKey));
await tester.tap(find.byKey(buttonKey)); await tester.tap(find.byKey(buttonKey));
...@@ -477,7 +482,7 @@ void main() { ...@@ -477,7 +482,7 @@ void main() {
final Key buttonKey = UniqueKey(); final Key buttonKey = UniqueKey();
String value; String value;
Widget build() => buildFrame(buttonKey: buttonKey, value: value); Widget build() => buildFrame(buttonKey: buttonKey, value: value, onChanged: onChanged);
await tester.pumpWidget(build()); await tester.pumpWidget(build());
final RenderBox buttonBoxNullValue = tester.renderObject(find.byKey(buttonKey)); final RenderBox buttonBoxNullValue = tester.renderObject(find.byKey(buttonKey));
...@@ -594,19 +599,19 @@ void main() { ...@@ -594,19 +599,19 @@ void main() {
// so that it fits within the frame. // so that it fits within the frame.
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.topLeft, value: menuItems.last) buildFrame(alignment: Alignment.topLeft, value: menuItems.last, onChanged: onChanged)
); );
expect(menuRect.topLeft, Offset.zero); expect(menuRect.topLeft, Offset.zero);
expect(menuRect.topRight, Offset(menuRect.width, 0.0)); expect(menuRect.topRight, Offset(menuRect.width, 0.0));
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.topCenter, value: menuItems.last) buildFrame(alignment: Alignment.topCenter, value: menuItems.last, onChanged: onChanged)
); );
expect(menuRect.topLeft, Offset(buttonRect.left, 0.0)); expect(menuRect.topLeft, Offset(buttonRect.left, 0.0));
expect(menuRect.topRight, Offset(buttonRect.right, 0.0)); expect(menuRect.topRight, Offset(buttonRect.right, 0.0));
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.topRight, value: menuItems.last) buildFrame(alignment: Alignment.topRight, value: menuItems.last, onChanged: onChanged)
); );
expect(menuRect.topLeft, Offset(800.0 - menuRect.width, 0.0)); expect(menuRect.topLeft, Offset(800.0 - menuRect.width, 0.0));
expect(menuRect.topRight, const Offset(800.0, 0.0)); expect(menuRect.topRight, const Offset(800.0, 0.0));
...@@ -616,19 +621,19 @@ void main() { ...@@ -616,19 +621,19 @@ void main() {
// is selected) and shifted horizontally so that it fits within the frame. // is selected) and shifted horizontally so that it fits within the frame.
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.centerLeft, value: menuItems.first) buildFrame(alignment: Alignment.centerLeft, value: menuItems.first, onChanged: onChanged)
); );
expect(menuRect.topLeft, Offset(0.0, buttonRect.top)); expect(menuRect.topLeft, Offset(0.0, buttonRect.top));
expect(menuRect.topRight, Offset(menuRect.width, buttonRect.top)); expect(menuRect.topRight, Offset(menuRect.width, buttonRect.top));
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.center, value: menuItems.first) buildFrame(alignment: Alignment.center, value: menuItems.first, onChanged: onChanged)
); );
expect(menuRect.topLeft, buttonRect.topLeft); expect(menuRect.topLeft, buttonRect.topLeft);
expect(menuRect.topRight, buttonRect.topRight); expect(menuRect.topRight, buttonRect.topRight);
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.centerRight, value: menuItems.first) buildFrame(alignment: Alignment.centerRight, value: menuItems.first, onChanged: onChanged)
); );
expect(menuRect.topLeft, Offset(800.0 - menuRect.width, buttonRect.top)); expect(menuRect.topLeft, Offset(800.0 - menuRect.width, buttonRect.top));
expect(menuRect.topRight, Offset(800.0, buttonRect.top)); expect(menuRect.topRight, Offset(800.0, buttonRect.top));
...@@ -638,26 +643,26 @@ void main() { ...@@ -638,26 +643,26 @@ void main() {
// so that it fits within the frame. // so that it fits within the frame.
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.bottomLeft, value: menuItems.first) buildFrame(alignment: Alignment.bottomLeft, value: menuItems.first, onChanged: onChanged)
); );
expect(menuRect.bottomLeft, const Offset(0.0, 600.0)); expect(menuRect.bottomLeft, const Offset(0.0, 600.0));
expect(menuRect.bottomRight, Offset(menuRect.width, 600.0)); expect(menuRect.bottomRight, Offset(menuRect.width, 600.0));
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.bottomCenter, value: menuItems.first) buildFrame(alignment: Alignment.bottomCenter, value: menuItems.first, onChanged: onChanged)
); );
expect(menuRect.bottomLeft, Offset(buttonRect.left, 600.0)); expect(menuRect.bottomLeft, Offset(buttonRect.left, 600.0));
expect(menuRect.bottomRight, Offset(buttonRect.right, 600.0)); expect(menuRect.bottomRight, Offset(buttonRect.right, 600.0));
await popUpAndDown( await popUpAndDown(
buildFrame(alignment: Alignment.bottomRight, value: menuItems.first) buildFrame(alignment: Alignment.bottomRight, value: menuItems.first, onChanged: onChanged)
); );
expect(menuRect.bottomLeft, Offset(800.0 - menuRect.width, 600.0)); expect(menuRect.bottomLeft, Offset(800.0 - menuRect.width, 600.0));
expect(menuRect.bottomRight, const Offset(800.0, 600.0)); expect(menuRect.bottomRight, const Offset(800.0, 600.0));
}); });
testWidgets('Dropdown menus are dismissed on screen orientation changes', (WidgetTester tester) async { testWidgets('Dropdown menus are dismissed on screen orientation changes', (WidgetTester tester) async {
await tester.pumpWidget(buildFrame()); await tester.pumpWidget(buildFrame(onChanged: onChanged));
await tester.tap(find.byType(dropdownButtonType)); await tester.tap(find.byType(dropdownButtonType));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.byType(ListView), findsOneWidget); expect(find.byType(ListView), findsOneWidget);
...@@ -670,7 +675,7 @@ void main() { ...@@ -670,7 +675,7 @@ void main() {
testWidgets('Semantics Tree contains only selected element', (WidgetTester tester) async { testWidgets('Semantics Tree contains only selected element', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(buildFrame(items: menuItems)); await tester.pumpWidget(buildFrame(items: menuItems, onChanged: onChanged));
expect(semantics, isNot(includesNodeWith(label: menuItems[0]))); expect(semantics, isNot(includesNodeWith(label: menuItems[0])));
expect(semantics, includesNodeWith(label: menuItems[1])); expect(semantics, includesNodeWith(label: menuItems[1]));
...@@ -702,7 +707,7 @@ void main() { ...@@ -702,7 +707,7 @@ void main() {
buttonKey: key, buttonKey: key,
value: 'three', value: 'three',
items: menuItems, items: menuItems,
onChanged: null, onChanged: onChanged,
hint: const Text('test'), hint: const Text('test'),
)); ));
...@@ -722,6 +727,7 @@ void main() { ...@@ -722,6 +727,7 @@ void main() {
buttonKey: key, buttonKey: key,
value: null, value: null,
items: menuItems, items: menuItems,
onChanged: onChanged,
)); ));
await tester.tap(find.byKey(key)); await tester.tap(find.byKey(key));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
...@@ -778,6 +784,42 @@ void main() { ...@@ -778,6 +784,42 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('disabledHint displays on empty items or onChanged', (WidgetTester tester) async {
final Key buttonKey = UniqueKey();
Widget build({List<String> items, ValueChanged<String> onChanged}) => buildFrame(
items: items,
onChanged: onChanged,
buttonKey: buttonKey, value: null,
hint: const Text('enabled'),
disabledHint: const Text('disabled'));
// [disabledHint] should display when [items] is null
await tester.pumpWidget(build(items: null, onChanged: onChanged));
expect(find.text('enabled'), findsNothing);
expect(find.text('disabled'), findsOneWidget);
// [disabledHint] should display when [items] is an empty list.
await tester.pumpWidget(build(items: <String>[], onChanged: onChanged));
expect(find.text('enabled'), findsNothing);
expect(find.text('disabled'), findsOneWidget);
// [disabledHint] should display when [onChanged] is null
await tester.pumpWidget(build(items: menuItems, onChanged: null));
expect(find.text('enabled'), findsNothing);
expect(find.text('disabled'), findsOneWidget);
final RenderBox disabledHintBox = tester.renderObject(find.byKey(buttonKey));
// A Dropdown button with a disabled hint should be the same size as a
// one with a regular enabled hint.
await tester.pumpWidget(build(items: menuItems, onChanged: onChanged));
expect(find.text('disabled'), findsNothing);
expect(find.text('enabled'), findsOneWidget);
final RenderBox enabledHintBox = tester.renderObject(find.byKey(buttonKey));
expect(enabledHintBox.localToGlobal(Offset.zero), equals(disabledHintBox.localToGlobal(Offset.zero)));
expect(enabledHintBox.size, equals(disabledHintBox.size));
});
testWidgets('Dropdown in middle showing middle item', (WidgetTester tester) async { testWidgets('Dropdown in middle showing middle item', (WidgetTester tester) async {
final List<DropdownMenuItem<int>> items = final List<DropdownMenuItem<int>> items =
List<DropdownMenuItem<int>>.generate(100, (int i) => List<DropdownMenuItem<int>>.generate(100, (int i) =>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment