Commit 6a1ac731 authored by Hans Muller's avatar Hans Muller Committed by GitHub

PopupMenuButton menus inherit the button theme (#6132)

parent d3d9e1ca
......@@ -246,6 +246,7 @@ class _MaterialAppState extends State<MaterialApp> {
ThemeData theme = config.theme ?? new ThemeData.fallback();
Widget result = new AnimatedTheme(
data: theme,
isMaterialAppTheme: true,
child: new WidgetsApp(
key: new GlobalObjectKey(this),
title: config.title,
......
......@@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material.dart';
import 'theme.dart';
const Duration _kBottomSheetDuration = const Duration(milliseconds: 200);
const double _kMinFlingVelocity = 700.0;
......@@ -204,10 +205,12 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
_ModalBottomSheetRoute({
Completer<T> completer,
this.builder
this.builder,
this.theme,
}) : super(completer: completer);
final WidgetBuilder builder;
final ThemeData theme;
@override
Duration get transitionDuration => _kBottomSheetDuration;
......@@ -229,7 +232,10 @@ class _ModalBottomSheetRoute<T> extends PopupRoute<T> {
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return new _ModalBottomSheet<T>(route: this);
Widget bottomSheet = new _ModalBottomSheet<T>(route: this);
if (theme != null)
bottomSheet = new Theme(data: theme, child: bottomSheet);
return bottomSheet;
}
}
......@@ -257,7 +263,8 @@ Future<dynamic/*=T*/> showModalBottomSheet/*<T>*/({ BuildContext context, Widget
final Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>();
Navigator.push(context, new _ModalBottomSheetRoute<dynamic/*=T*/>(
completer: completer,
builder: builder
builder: builder,
theme: Theme.of(context, shadowThemeOnly: true),
));
return completer.future;
}
......@@ -280,10 +280,12 @@ class SimpleDialog extends StatelessWidget {
class _DialogRoute<T> extends PopupRoute<T> {
_DialogRoute({
Completer<T> completer,
this.child
this.child,
this.theme,
}) : super(completer: completer);
final Widget child;
final ThemeData theme;
@override
Duration get transitionDuration => const Duration(milliseconds: 150);
......@@ -296,7 +298,7 @@ class _DialogRoute<T> extends PopupRoute<T> {
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return child;
return theme != null ? new Theme(data: theme, child: child) : child;
}
@override
......@@ -324,6 +326,10 @@ class _DialogRoute<T> extends PopupRoute<T> {
/// * <https://www.google.com/design/spec/components/dialogs.html>
Future<dynamic/*=T*/> showDialog/*<T>*/({ BuildContext context, Widget child }) {
Completer<dynamic/*=T*/> completer = new Completer<dynamic/*=T*/>();
Navigator.push(context, new _DialogRoute<dynamic/*=T*/>(completer: completer, child: child));
Navigator.push(context, new _DialogRoute<dynamic/*=T*/>(
completer: completer,
child: child,
theme: Theme.of(context, shadowThemeOnly: true),
));
return completer.future;
}
......@@ -293,7 +293,8 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
this.buttonRect,
this.selectedIndex,
this.elevation: 8,
TextStyle style
this.theme,
TextStyle style,
}) : _style = style, super(completer: completer) {
assert(style != null);
}
......@@ -303,6 +304,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
final Rect buttonRect;
final int selectedIndex;
final int elevation;
final ThemeData theme;
// The layout gets this route's scrollableKey so that it can scroll the
/// selected item into position, but only on the initial layout.
bool initialLayout = true;
......@@ -329,9 +331,13 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
Widget menu = new _DropdownMenu<T>(route: this);
if (theme != null)
menu = new Theme(data: theme, child: menu);
return new CustomSingleChildLayout(
delegate: new _DropdownMenuRouteLayout<T>(route: this),
child: new _DropdownMenu<T>(route: this)
child: menu,
);
}
}
......@@ -501,7 +507,8 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> {
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
selectedIndex: _selectedIndex,
elevation: config.elevation,
style: _textStyle
theme: Theme.of(context, shadowThemeOnly: true),
style: _textStyle,
);
Navigator.push(context, _currentRoute);
completer.future.then((_DropdownRouteResult<T> newValue) {
......
......@@ -374,13 +374,15 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
this.position,
this.items,
this.initialValue,
this.elevation
this.elevation,
this.theme
}) : super(completer: completer);
final RelativeRect position;
final List<PopupMenuEntry<T>> items;
final dynamic initialValue;
final int elevation;
final ThemeData theme;
@override
Animation<double> createAnimation() {
......@@ -411,9 +413,14 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
selectedItemOffset += items[i].height;
}
}
Widget menu = new _PopupMenu<T>(route: this);
if (theme != null)
menu = new Theme(data: theme, child: menu);
return new CustomSingleChildLayout(
delegate: new _PopupMenuRouteLayout(position, selectedItemOffset),
child: new _PopupMenu<T>(route: this)
child: menu
);
}
}
......@@ -438,7 +445,8 @@ Future<dynamic/*=T*/> showMenu/*<T>*/({
position: position,
items: items,
initialValue: initialValue,
elevation: elevation
elevation: elevation,
theme: Theme.of(context, shadowThemeOnly: true),
));
return completer.future;
}
......
......@@ -18,6 +18,7 @@ const Duration kThemeAnimationDuration = const Duration(milliseconds: 200);
///
/// * [AnimatedTheme]
/// * [ThemeData]
/// * [MaterialApp]
class Theme extends InheritedWidget {
/// Applies the given theme [data] to [child].
///
......@@ -25,6 +26,7 @@ class Theme extends InheritedWidget {
Theme({
Key key,
@required this.data,
this.isMaterialAppTheme: false,
Widget child
}) : super(key: key, child: child) {
assert(child != null);
......@@ -34,14 +36,33 @@ class Theme extends InheritedWidget {
/// Specifies the color and typography values for descendant widgets.
final ThemeData data;
/// True if this theme was installed by the [MaterialApp].
///
/// When an app uses the [Navigator] to push a route, the route's widgets
/// will only inherit from the app's theme, even though the widget that
/// triggered the push may inherit from a theme that "shadows" the app's
/// theme because it's deeper in the widget tree. Apps can find the shadowing
/// theme with `Theme.of(context, shadowThemeOnly: true)` and pass it along
/// to the class that creates a route's widgets. Material widgets that push
/// routes, like [PopupMenuButton] and [DropdownButton], do this.
final bool isMaterialAppTheme;
static final ThemeData _kFallbackTheme = new ThemeData.fallback();
/// The data from the closest instance of this class that encloses the given context.
///
/// Defaults to the fallback theme data if none exists.
static ThemeData of(BuildContext context) {
Theme theme = context.inheritFromWidgetOfExactType(Theme);
return theme?.data ?? _kFallbackTheme;
///
/// If [shadowThemeOnly] is true and the closest Theme ancestor was installed by
/// the [MaterialApp] - in other words if the closest Theme ancestor does not
/// shadow the app's theme - then return null. This property is specified in
/// situations where its useful to wrap a route's widgets with a Theme, but only
/// when the app's theme is being shadowed by a theme widget that is farather
/// down in the tree. See [isMaterialAppTheme].
static ThemeData of(BuildContext context, { bool shadowThemeOnly: false }) {
final Theme theme = context.inheritFromWidgetOfExactType(Theme);
final ThemeData themeData = theme?.data ?? _kFallbackTheme;
return shadowThemeOnly ? (theme.isMaterialAppTheme ? null : themeData) : themeData;
}
@override
......@@ -77,6 +98,7 @@ class AnimatedTheme extends ImplicitlyAnimatedWidget {
AnimatedTheme({
Key key,
@required this.data,
this.isMaterialAppTheme: false,
Curve curve: Curves.linear,
Duration duration: kThemeAnimationDuration,
this.child
......@@ -88,6 +110,9 @@ class AnimatedTheme extends ImplicitlyAnimatedWidget {
/// Specifies the color and typography values for descendant widgets.
final ThemeData data;
/// True if this theme was created by the [MaterialApp]. See [Theme.isMaterialAppTheme].
final bool isMaterialAppTheme;
/// The widget below this widget in the tree.
final Widget child;
......@@ -108,6 +133,7 @@ class _AnimatedThemeState extends AnimatedWidgetBaseState<AnimatedTheme> {
@override
Widget build(BuildContext context) {
return new Theme(
isMaterialAppTheme: config.isMaterialAppTheme,
child: config.child,
data: _data.evaluate(animation)
);
......
// Copyright 2016 The Chromium 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_test/flutter_test.dart';
void main() {
testWidgets('PopupMenu inherits app theme', (WidgetTester tester) async {
final Key popupMenuButtonKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new PopupMenuButton<String>(
key: popupMenuButtonKey,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
new PopupMenuItem<String>(child: new Text('menuItem'))
];
}
),
]
)
)
)
);
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark));
});
testWidgets('PopupMenu inherits shadowed app theme', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/5572
final Key popupMenuButtonKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new PopupMenuButton<String>(
key: popupMenuButtonKey,
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
new PopupMenuItem<String>(child: new Text('menuItem'))
];
}
),
]
)
)
)
)
);
await tester.tap(find.byKey(popupMenuButtonKey));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.light));
});
testWidgets('DropdownMenu inherits shadowed app theme', (WidgetTester tester) async {
final Key dropdownMenuButtonKey = new UniqueKey();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
appBar: new AppBar(
actions: <Widget>[
new DropdownButton<String>(
key: dropdownMenuButtonKey,
onChanged: (String newValue) { },
value: 'menuItem',
items: <DropdownMenuItem<String>>[
new DropdownMenuItem<String>(
value: 'menuItem',
child: new Text('menuItem'),
),
],
)
]
)
)
)
)
);
await tester.tap(find.byKey(dropdownMenuButtonKey));
await tester.pump(const Duration(seconds: 1));
for(Element item in tester.elementList(find.text('menuItem')))
expect(Theme.of(item).brightness, equals(Brightness.light));
});
testWidgets('ModalBottomSheet inherits shadowed app theme', (WidgetTester tester) async {
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
body: new Center(
child: new Builder(
builder: (BuildContext context) {
return new RaisedButton(
onPressed: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) => new Text('bottomSheet'),
);
},
child: new Text('SHOW'),
);
}
)
)
)
)
)
);
await tester.tap(find.text('SHOW'));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('bottomSheet'))).brightness, equals(Brightness.light));
await tester.tap(find.text('bottomSheet')); // dismiss the bottom sheet
await tester.pump(const Duration(seconds: 1));
});
testWidgets('Dialog inherits shadowed app theme', (WidgetTester tester) async {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>();
await tester.pumpWidget(
new MaterialApp(
theme: new ThemeData(brightness: Brightness.dark),
home: new Theme(
data: new ThemeData(brightness: Brightness.light),
child: new Scaffold(
key: scaffoldKey,
body: new Center(
child: new Builder(
builder: (BuildContext context) {
return new RaisedButton(
onPressed: () {
showDialog(
context: context,
child: new Text('dialog'),
);
},
child: new Text('SHOW'),
);
}
)
)
)
)
)
);
await tester.tap(find.text('SHOW'));
await tester.pump(const Duration(seconds: 1));
expect(Theme.of(tester.element(find.text('dialog'))).brightness, equals(Brightness.light));
});
}
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