Merge pull request #1247 from Hixie/menu
Introduce a showPopupMenu() function
diff --git a/examples/stocks/lib/main.dart b/examples/stocks/lib/main.dart
index 117f1a1..a70739b 100644
--- a/examples/stocks/lib/main.dart
+++ b/examples/stocks/lib/main.dart
@@ -4,6 +4,7 @@
library stocks;
+import 'dart:async';
import 'dart:math' as math;
import 'dart:sky' as sky;
diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart
index 0cecb05..3c65981 100644
--- a/examples/stocks/lib/stock_home.dart
+++ b/examples/stocks/lib/stock_home.dart
@@ -74,28 +74,6 @@
});
}
- 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;
void _handleAutorefreshChanged(bool value) {
setState(() {
@@ -112,6 +90,13 @@
return EventDisposition.processed;
}
+ void _handleMenuShow() {
+ showStockMenu(navigator,
+ autorefresh: _autorefresh,
+ onAutorefreshChanged: _handleAutorefreshChanged
+ );
+ }
+
Drawer buildDrawer() {
if (_drawerStatus == AnimationStatus.dismissed)
return null;
@@ -282,31 +267,13 @@
);
}
- 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() {
- List<Widget> overlays = [
- new Scaffold(
- toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
- body: buildTabNavigator(),
- snackBar: buildSnackBar(),
- floatingActionButton: buildFloatingActionButton(),
- drawer: buildDrawer()
- ),
- ];
- addMenuToOverlays(overlays);
- return new Stack(overlays);
+ return new Scaffold(
+ toolbar: _isSearching ? buildSearchBar() : buildToolBar(),
+ body: buildTabNavigator(),
+ snackBar: buildSnackBar(),
+ floatingActionButton: buildFloatingActionButton(),
+ drawer: buildDrawer()
+ );
}
}
diff --git a/examples/stocks/lib/stock_menu.dart b/examples/stocks/lib/stock_menu.dart
index ee8df62..a61073a 100644
--- a/examples/stocks/lib/stock_menu.dart
+++ b/examples/stocks/lib/stock_menu.dart
@@ -4,45 +4,29 @@
part of stocks;
-class StockMenu extends Component {
- StockMenu({
- Key key,
- this.showing,
- 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
- ),
+Future showStockMenu(Navigator navigator, { bool autorefresh, ValueChanged onAutorefreshChanged }) {
+ return showMenu(
+ navigator: navigator,
+ position: new MenuPosition(
right: sky.view.paddingRight,
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
diff --git a/sky/packages/sky/lib/src/widgets/dialog.dart b/sky/packages/sky/lib/src/widgets/dialog.dart
index da5b194..7be95f8 100644
--- a/sky/packages/sky/lib/src/widgets/dialog.dart
+++ b/sky/packages/sky/lib/src/widgets/dialog.dart
@@ -141,11 +141,11 @@
Duration get transitionDuration => _kTransitionDuration;
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(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
- child: builder(navigator, route)
+ child: builder(navigator, this)
);
}
diff --git a/sky/packages/sky/lib/src/widgets/modal_overlay.dart b/sky/packages/sky/lib/src/widgets/modal_overlay.dart
deleted file mode 100644
index 16ed10f..0000000
--- a/sky/packages/sky/lib/src/widgets/modal_overlay.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-// 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)
- );
- }
-
-}
diff --git a/sky/packages/sky/lib/src/widgets/navigator.dart b/sky/packages/sky/lib/src/widgets/navigator.dart
index 7eeac86..b4f5703 100644
--- a/sky/packages/sky/lib/src/widgets/navigator.dart
+++ b/sky/packages/sky/lib/src/widgets/navigator.dart
@@ -49,7 +49,7 @@
Duration get transitionDuration;
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); }
String toString() => '$runtimeType()';
@@ -67,7 +67,7 @@
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): Block input unless content is interactive
return new SlideTransition(
@@ -77,7 +77,7 @@
child: new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: easeOut),
- child: builder(navigator, route)
+ child: builder(navigator, this)
)
);
}
@@ -102,7 +102,7 @@
bool get hasContent => false;
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 {
@@ -202,11 +202,17 @@
});
};
Key key = new ObjectKey(route);
- Widget widget = route.build(key, this, route, performance);
+ Widget widget = route.build(key, this, performance);
visibleRoutes.add(widget);
if (route.isActuallyOpaque)
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()));
}
}
diff --git a/sky/packages/sky/lib/src/widgets/popup_menu.dart b/sky/packages/sky/lib/src/widgets/popup_menu.dart
index 4dac20a..54fcc12 100644
--- a/sky/packages/sky/lib/src/widgets/popup_menu.dart
+++ b/sky/packages/sky/lib/src/widgets/popup_menu.dart
@@ -2,12 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:async';
import 'dart:sky' as sky;
import 'package:sky/animation.dart';
import 'package:sky/painting.dart';
import 'package:sky/material.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/navigator.dart';
import 'package:sky/src/widgets/popup_menu_item.dart';
@@ -23,80 +25,75 @@
const double _kMenuHorizontalPadding = 16.0;
const double _kMenuVerticalPadding = 8.0;
-typedef void PopupMenuDismissedCallback();
-
class PopupMenu extends StatefulComponent {
PopupMenu({
Key key,
- this.showing,
- this.onDismissed,
this.items,
- this.level,
- this.navigator
- }) : super(key: key);
+ this.level: 4,
+ this.navigator,
+ this.performance
+ }) : super(key: key) {
+ assert(items != null);
+ assert(performance != null);
+ }
- bool showing;
- PopupMenuDismissedCallback onDismissed;
List<PopupMenuItem> items;
int level;
Navigator navigator;
+ WatchableAnimationPerformance performance;
- AnimationPerformance _performance;
+ BoxPainter _painter;
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();
+ }
- if (showing)
- _open();
+ void _updateBoxPainter() {
+ _painter = new BoxPainter(
+ new BoxDecoration(
+ backgroundColor: Colors.grey[50],
+ borderRadius: 2.0,
+ boxShadow: shadows[level]
+ )
+ );
}
void syncConstructorArguments(PopupMenu source) {
- if (!showing && source.showing)
- _open();
- showing = source.showing;
+ items = source.items;
if (level != source.level) {
level = source.level;
_updateBoxPainter();
}
- items = source.items;
navigator = source.navigator;
+ if (mounted)
+ performance.removeListener(_performanceChanged);
+ performance = source.performance;
+ if (mounted)
+ performance.addListener(_performanceChanged);
}
- void _open() {
- navigator.pushState(this, (_) => _close());
- _performance.play();
+ void didMount() {
+ performance.addListener(_performanceChanged);
+ super.didMount();
}
- void _close() {
- _performance.reverse();
+ void didUnmount() {
+ performance.removeListener(_performanceChanged);
+ super.didMount();
}
- void _updateBoxPainter() {
- _painter = new BoxPainter(new BoxDecoration(
- backgroundColor: Colors.grey[50],
- borderRadius: 2.0,
- boxShadow: shadows[level]));
+ void _performanceChanged() {
+ setState(() {
+ // the performance changed, and our state is tied up with the performance
+ });
}
- void _handleDismissed() {
- if (navigator != null &&
- navigator.currentRoute is RouteState &&
- (navigator.currentRoute as RouteState).owner == this) // TODO(ianh): remove cast once analyzer is cleverer
- navigator.pop();
- if (onDismissed != null)
- onDismissed();
+ void itemPressed(PopupMenuItem item) {
+ if (navigator != null)
+ navigator.pop(item.value);
}
- BoxPainter _painter;
-
Widget build() {
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 = [];
@@ -104,21 +101,20 @@
double start = (i + 1) * unit;
double end = (start + 1.5 * unit).clamp(0.0, 1.0);
children.add(new FadeTransition(
- performance: _performance.view,
+ performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(start, end)),
child: items[i])
);
}
-
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));
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)),
child: new Container(
margin: new EdgeDims.all(_kMenuMargin),
child: new BuilderTransition(
- performance: _performance.view,
+ performance: performance,
variables: [width, height],
builder: () {
return new CustomPaint(
@@ -153,3 +149,67 @@
}
}
+
+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;
+}
diff --git a/sky/packages/sky/lib/src/widgets/popup_menu_item.dart b/sky/packages/sky/lib/src/widgets/popup_menu_item.dart
index b787c2a..f55cad2 100644
--- a/sky/packages/sky/lib/src/widgets/popup_menu_item.dart
+++ b/sky/packages/sky/lib/src/widgets/popup_menu_item.dart
@@ -8,6 +8,7 @@
import 'package:sky/src/widgets/framework.dart';
import 'package:sky/src/widgets/gesture_detector.dart';
import 'package:sky/src/widgets/ink_well.dart';
+import 'package:sky/src/widgets/popup_menu.dart';
import 'package:sky/src/widgets/theme.dart';
const double _kMenuItemHeight = 48.0;
@@ -17,17 +18,33 @@
PopupMenuItem({
Key key,
this.onPressed,
+ this.value,
this.child
}) : super(key: key);
final Widget child;
final Function onPressed;
+ final dynamic value;
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() {
return new GestureDetector(
- onTap: onPressed,
+ onTap: handlePressed,
child: new InkWell(
child: new Container(
height: _kMenuItemHeight,
diff --git a/sky/packages/sky/lib/widgets.dart b/sky/packages/sky/lib/widgets.dart
index 738195a..6df19bd 100644
--- a/sky/packages/sky/lib/widgets.dart
+++ b/sky/packages/sky/lib/widgets.dart
@@ -36,7 +36,6 @@
export 'package:sky/src/widgets/material_button.dart';
export 'package:sky/src/widgets/mimic.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/popup_menu.dart';
export 'package:sky/src/widgets/popup_menu_item.dart';