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';