Commit a3ae46b9 authored by Hixie's avatar Hixie

Introduce a showPopupMenu() function

Instead of having to manage the popup menu from your app's build
function, you now just call showPopupMenu() with the menu's position and
it takes care of everything for you.

This solves the problem that the popup menu was trying to mutate the
state of the navigator from within its own initState() function.

Also, remove the "route" argument to RouteBase.build() since it equals
"this" by definition...

Also, remove ModalOverlay, and instead put that logic in the navigator.
parent f15b9480
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
library stocks; library stocks;
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:sky' as sky; import 'dart:sky' as sky;
......
...@@ -74,28 +74,6 @@ class StockHome extends StatefulComponent { ...@@ -74,28 +74,6 @@ class StockHome extends StatefulComponent {
}); });
} }
bool _menuShowing = false;
AnimationStatus _menuStatus = AnimationStatus.dismissed;
void _handleMenuShow() {
setState(() {
_menuShowing = true;
_menuStatus = AnimationStatus.forward;
});
}
void _handleMenuHide() {
setState(() {
_menuShowing = false;
});
}
void _handleMenuDismissed() {
setState(() {
_menuStatus = AnimationStatus.dismissed;
});
}
bool _autorefresh = false; bool _autorefresh = false;
void _handleAutorefreshChanged(bool value) { void _handleAutorefreshChanged(bool value) {
setState(() { setState(() {
...@@ -112,6 +90,13 @@ class StockHome extends StatefulComponent { ...@@ -112,6 +90,13 @@ class StockHome extends StatefulComponent {
return EventDisposition.processed; return EventDisposition.processed;
} }
void _handleMenuShow() {
showStockMenu(navigator,
autorefresh: _autorefresh,
onAutorefreshChanged: _handleAutorefreshChanged
);
}
Drawer buildDrawer() { Drawer buildDrawer() {
if (_drawerStatus == AnimationStatus.dismissed) if (_drawerStatus == AnimationStatus.dismissed)
return null; return null;
...@@ -282,31 +267,13 @@ class StockHome extends StatefulComponent { ...@@ -282,31 +267,13 @@ class StockHome extends StatefulComponent {
); );
} }
void addMenuToOverlays(List<Widget> overlays) {
if (_menuStatus == AnimationStatus.dismissed)
return;
overlays.add(new ModalOverlay(
children: [new StockMenu(
showing: _menuShowing,
onDismissed: _handleMenuDismissed,
navigator: navigator,
autorefresh: _autorefresh,
onAutorefreshChanged: _handleAutorefreshChanged
)],
onDismiss: _handleMenuHide));
}
Widget build() { Widget build() {
List<Widget> overlays = [ return new Scaffold(
new Scaffold( toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
toolbar: _isSearching ? buildSearchBar() : buildToolBar(), body: buildTabNavigator(),
body: buildTabNavigator(), snackBar: buildSnackBar(),
snackBar: buildSnackBar(), floatingActionButton: buildFloatingActionButton(),
floatingActionButton: buildFloatingActionButton(), drawer: buildDrawer()
drawer: buildDrawer() );
),
];
addMenuToOverlays(overlays);
return new Stack(overlays);
} }
} }
...@@ -4,45 +4,29 @@ ...@@ -4,45 +4,29 @@
part of stocks; part of stocks;
class StockMenu extends Component { Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) {
StockMenu({ return showMenu(
Key key, navigator: navigator,
this.showing, position: new MenuPosition(
this.onDismissed,
this.navigator,
this.autorefresh: false,
this.onAutorefreshChanged
}) : super(key: key);
final bool showing;
final PopupMenuDismissedCallback onDismissed;
final Navigator navigator;
final bool autorefresh;
final ValueChanged onAutorefreshChanged;
Widget build() {
var checkbox = new Checkbox(
value: this.autorefresh,
onChanged: this.onAutorefreshChanged
);
return new Positioned(
child: new PopupMenu(
items: [
new PopupMenuItem(child: new Text('Add stock')),
new PopupMenuItem(child: new Text('Remove stock')),
new PopupMenuItem(
onPressed: () => onAutorefreshChanged(!autorefresh),
child: new Row([new Flexible(child: new Text('Autorefresh')), checkbox])
),
],
level: 4,
showing: showing,
onDismissed: onDismissed,
navigator: navigator
),
right: sky.view.paddingRight, right: sky.view.paddingRight,
top: sky.view.paddingTop top: sky.view.paddingTop
); ),
} builder: (Navigator navigator) {
} return <PopupMenuItem>[
new PopupMenuItem(child: new Text('Add stock')),
new PopupMenuItem(child: new Text('Remove stock')),
new PopupMenuItem(
onPressed: () => onAutorefreshChanged(!autorefresh),
child: new Row([
new Flexible(child: new Text('Autorefresh')),
new Checkbox(
value: autorefresh,
onChanged: onAutorefreshChanged
)
]
)
),
];
}
);
}
\ No newline at end of file
...@@ -141,11 +141,11 @@ class DialogRoute extends RouteBase { ...@@ -141,11 +141,11 @@ class DialogRoute extends RouteBase {
Duration get transitionDuration => _kTransitionDuration; Duration get transitionDuration => _kTransitionDuration;
bool get isOpaque => false; bool get isOpaque => false;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) { Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
return new FadeTransition( return new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut), opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: builder(navigator, route) child: builder(navigator, this)
); );
} }
......
// Copyright 2015 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:sky/src/widgets/basic.dart';
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/gesture_detector.dart';
class ModalOverlay extends Component {
ModalOverlay({ Key key, this.children, this.onDismiss }) : super(key: key);
final List<Widget> children;
final Function onDismiss;
Widget build() {
return new GestureDetector(
onTap: onDismiss,
child: new Stack(children)
);
}
}
...@@ -49,7 +49,7 @@ abstract class RouteBase { ...@@ -49,7 +49,7 @@ abstract class RouteBase {
Duration get transitionDuration; Duration get transitionDuration;
bool get isOpaque; bool get isOpaque;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance); Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance);
void popState([dynamic result]) { assert(result == null); } void popState([dynamic result]) { assert(result == null); }
String toString() => '$runtimeType()'; String toString() => '$runtimeType()';
...@@ -67,7 +67,7 @@ class Route extends RouteBase { ...@@ -67,7 +67,7 @@ class Route extends RouteBase {
Duration get transitionDuration => _kTransitionDuration; Duration get transitionDuration => _kTransitionDuration;
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) { Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
// TODO(jackson): Hit testing should ignore transform // TODO(jackson): Hit testing should ignore transform
// TODO(jackson): Block input unless content is interactive // TODO(jackson): Block input unless content is interactive
return new SlideTransition( return new SlideTransition(
...@@ -77,7 +77,7 @@ class Route extends RouteBase { ...@@ -77,7 +77,7 @@ class Route extends RouteBase {
child: new FadeTransition( child: new FadeTransition(
performance: performance, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut), opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
child: builder(navigator, route) child: builder(navigator, this)
) )
); );
} }
...@@ -102,7 +102,7 @@ class RouteState extends RouteBase { ...@@ -102,7 +102,7 @@ class RouteState extends RouteBase {
bool get hasContent => false; bool get hasContent => false;
Duration get transitionDuration => const Duration(); Duration get transitionDuration => const Duration();
Widget build(Key key, Navigator navigator, RouteBase route, WatchableAnimationPerformance performance) => null; Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) => null;
} }
class NavigationState { class NavigationState {
...@@ -202,11 +202,17 @@ class Navigator extends StatefulComponent { ...@@ -202,11 +202,17 @@ class Navigator extends StatefulComponent {
}); });
}; };
Key key = new ObjectKey(route); Key key = new ObjectKey(route);
Widget widget = route.build(key, this, route, performance); Widget widget = route.build(key, this, performance);
visibleRoutes.add(widget); visibleRoutes.add(widget);
if (route.isActuallyOpaque) if (route.isActuallyOpaque)
break; break;
} }
if (visibleRoutes.length > 1) {
visibleRoutes.insert(1, new Listener(
onPointerDown: (_) { pop(); return EventDisposition.consumed; },
child: new Container()
));
}
return new Focus(child: new Stack(visibleRoutes.reversed.toList())); return new Focus(child: new Stack(visibleRoutes.reversed.toList()));
} }
} }
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async';
import 'dart:sky' as sky; import 'dart:sky' as sky;
import 'package:sky/animation.dart'; import 'package:sky/animation.dart';
import 'package:sky/painting.dart'; import 'package:sky/painting.dart';
import 'package:sky/material.dart'; import 'package:sky/material.dart';
import 'package:sky/src/widgets/basic.dart'; import 'package:sky/src/widgets/basic.dart';
import 'package:sky/src/widgets/focus.dart';
import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/navigator.dart'; import 'package:sky/src/widgets/navigator.dart';
import 'package:sky/src/widgets/popup_menu_item.dart'; import 'package:sky/src/widgets/popup_menu_item.dart';
...@@ -23,80 +25,75 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep; ...@@ -23,80 +25,75 @@ const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuHorizontalPadding = 16.0; const double _kMenuHorizontalPadding = 16.0;
const double _kMenuVerticalPadding = 8.0; const double _kMenuVerticalPadding = 8.0;
typedef void PopupMenuDismissedCallback();
class PopupMenu extends StatefulComponent { class PopupMenu extends StatefulComponent {
PopupMenu({ PopupMenu({
Key key, Key key,
this.showing,
this.onDismissed,
this.items, this.items,
this.level, this.level: 4,
this.navigator this.navigator,
}) : super(key: key); this.performance
}) : super(key: key) {
assert(items != null);
assert(performance != null);
}
bool showing;
PopupMenuDismissedCallback onDismissed;
List<PopupMenuItem> items; List<PopupMenuItem> items;
int level; int level;
Navigator navigator; Navigator navigator;
WatchableAnimationPerformance performance;
AnimationPerformance _performance; BoxPainter _painter;
void initState() { void initState() {
_performance = new AnimationPerformance(duration: _kMenuDuration);
_performance.timing = new AnimationTiming()
..reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd);
_performance.addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed)
_handleDismissed();
});
_updateBoxPainter(); _updateBoxPainter();
}
if (showing) void _updateBoxPainter() {
_open(); _painter = new BoxPainter(
new BoxDecoration(
backgroundColor: Colors.grey[50],
borderRadius: 2.0,
boxShadow: shadows[level]
)
);
} }
void syncConstructorArguments(PopupMenu source) { void syncConstructorArguments(PopupMenu source) {
if (!showing && source.showing) items = source.items;
_open();
showing = source.showing;
if (level != source.level) { if (level != source.level) {
level = source.level; level = source.level;
_updateBoxPainter(); _updateBoxPainter();
} }
items = source.items;
navigator = source.navigator; navigator = source.navigator;
if (mounted)
performance.removeListener(_performanceChanged);
performance = source.performance;
if (mounted)
performance.addListener(_performanceChanged);
} }
void _open() { void didMount() {
navigator.pushState(this, (_) => _close()); performance.addListener(_performanceChanged);
_performance.play(); super.didMount();
} }
void _close() { void didUnmount() {
_performance.reverse(); performance.removeListener(_performanceChanged);
super.didMount();
} }
void _updateBoxPainter() { void _performanceChanged() {
_painter = new BoxPainter(new BoxDecoration( setState(() {
backgroundColor: Colors.grey[50], // the performance changed, and our state is tied up with the performance
borderRadius: 2.0, });
boxShadow: shadows[level]));
} }
void _handleDismissed() { void itemPressed(PopupMenuItem item) {
if (navigator != null && if (navigator != null)
navigator.currentRoute is RouteState && navigator.pop(item.value);
(navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
navigator.pop();
if (onDismissed != null)
onDismissed();
} }
BoxPainter _painter;
Widget build() { Widget build() {
double unit = 1.0 / (items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade. double unit = 1.0 / (items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
List<Widget> children = []; List<Widget> children = [];
...@@ -104,21 +101,20 @@ class PopupMenu extends StatefulComponent { ...@@ -104,21 +101,20 @@ class PopupMenu extends StatefulComponent {
double start = (i + 1) * unit; double start = (i + 1) * unit;
double end = (start + 1.5 * unit).clamp(0.0, 1.0); double end = (start + 1.5 * unit).clamp(0.0, 1.0);
children.add(new FadeTransition( children.add(new FadeTransition(
performance: _performance.view, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(start, end)), opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(start, end)),
child: items[i]) child: items[i])
); );
} }
final width = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit)); final width = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit));
final height = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit * items.length)); final height = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit * items.length));
return new FadeTransition( return new FadeTransition(
performance: _performance.view, performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, 1.0 / 3.0)), opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, 1.0 / 3.0)),
child: new Container( child: new Container(
margin: new EdgeDims.all(_kMenuMargin), margin: new EdgeDims.all(_kMenuMargin),
child: new BuilderTransition( child: new BuilderTransition(
performance: _performance.view, performance: performance,
variables: [width, height], variables: [width, height],
builder: () { builder: () {
return new CustomPaint( return new CustomPaint(
...@@ -153,3 +149,67 @@ class PopupMenu extends StatefulComponent { ...@@ -153,3 +149,67 @@ class PopupMenu extends StatefulComponent {
} }
} }
class MenuPosition {
const MenuPosition({ this.top, this.right, this.bottom, this.left });
final double top;
final double right;
final double bottom;
final double left;
}
class MenuRoute extends RouteBase {
MenuRoute({ this.completer, this.position, this.builder, this.level });
final Completer completer;
final MenuPosition position;
final PopupMenuItemsBuilder builder;
final int level;
AnimationPerformance createPerformance() {
AnimationPerformance result = super.createPerformance();
AnimationTiming timing = new AnimationTiming();
timing.reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd);
result.timing = timing;
return result;
}
Duration get transitionDuration => _kMenuDuration;
bool get isOpaque => false;
Widget build(Key key, Navigator navigator, WatchableAnimationPerformance performance) {
return new Positioned(
top: position?.top,
right: position?.right,
bottom: position?.bottom,
left: position?.left,
child: new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new PopupMenu(
key: key,
items: builder != null ? builder(navigator) : const <PopupMenuItem>[],
level: level,
navigator: navigator,
performance: performance
)
)
);
}
void popState([dynamic result]) {
completer.complete(result);
}
}
typedef List<PopupMenuItem> PopupMenuItemsBuilder(Navigator navigator);
Future showMenu({ Navigator navigator, MenuPosition position, PopupMenuItemsBuilder builder, int level: 4 }) {
Completer completer = new Completer();
navigator.push(new MenuRoute(
completer: completer,
position: position,
builder: builder,
level: level
));
return completer.future;
}
...@@ -8,6 +8,7 @@ import 'package:sky/src/widgets/default_text_style.dart'; ...@@ -8,6 +8,7 @@ import 'package:sky/src/widgets/default_text_style.dart';
import 'package:sky/src/widgets/framework.dart'; import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/gesture_detector.dart'; import 'package:sky/src/widgets/gesture_detector.dart';
import 'package:sky/src/widgets/ink_well.dart'; import 'package:sky/src/widgets/ink_well.dart';
import 'package:sky/src/widgets/popup_menu.dart';
import 'package:sky/src/widgets/theme.dart'; import 'package:sky/src/widgets/theme.dart';
const double _kMenuItemHeight = 48.0; const double _kMenuItemHeight = 48.0;
...@@ -17,17 +18,33 @@ class PopupMenuItem extends Component { ...@@ -17,17 +18,33 @@ class PopupMenuItem extends Component {
PopupMenuItem({ PopupMenuItem({
Key key, Key key,
this.onPressed, this.onPressed,
this.value,
this.child this.child
}) : super(key: key); }) : super(key: key);
final Widget child; final Widget child;
final Function onPressed; final Function onPressed;
final dynamic value;
TextStyle get textStyle => Theme.of(this).text.subhead; TextStyle get textStyle => Theme.of(this).text.subhead;
PopupMenu findAncestorPopupMenu() {
Widget ancestor = parent;
while (ancestor != null && ancestor is! PopupMenu)
ancestor = ancestor.parent;
return ancestor;
}
void handlePressed() {
if (onPressed != null)
onPressed();
PopupMenu menu = findAncestorPopupMenu();
menu?.itemPressed(this);
}
Widget build() { Widget build() {
return new GestureDetector( return new GestureDetector(
onTap: onPressed, onTap: handlePressed,
child: new InkWell( child: new InkWell(
child: new Container( child: new Container(
height: _kMenuItemHeight, height: _kMenuItemHeight,
......
...@@ -36,7 +36,6 @@ export 'package:sky/src/widgets/material.dart'; ...@@ -36,7 +36,6 @@ export 'package:sky/src/widgets/material.dart';
export 'package:sky/src/widgets/material_button.dart'; export 'package:sky/src/widgets/material_button.dart';
export 'package:sky/src/widgets/mimic.dart'; export 'package:sky/src/widgets/mimic.dart';
export 'package:sky/src/widgets/mixed_viewport.dart'; export 'package:sky/src/widgets/mixed_viewport.dart';
export 'package:sky/src/widgets/modal_overlay.dart';
export 'package:sky/src/widgets/navigator.dart'; export 'package:sky/src/widgets/navigator.dart';
export 'package:sky/src/widgets/popup_menu.dart'; export 'package:sky/src/widgets/popup_menu.dart';
export 'package:sky/src/widgets/popup_menu_item.dart'; export 'package:sky/src/widgets/popup_menu_item.dart';
......
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