// 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 'framework.dart'; import 'overlay.dart'; abstract class Route<T> { /// The navigator that the route is in, if any. NavigatorState get navigator => _navigator; NavigatorState _navigator; List<OverlayEntry> get overlayEntries => const <OverlayEntry>[]; /// Called when the route is inserted into the navigator. /// /// Use this to populate overlayEntries and add them to the overlay /// (accessible as navigator.overlay). (The reason the Route is responsible /// for doing this, rather than the Navigator, is that the Route will be /// responsible for _removing_ the entries and this way it's symmetric.) /// /// The overlay argument will be null if this is the first route inserted. void install(OverlayEntry insertionPoint) { } /// Called after install() when the route is pushed onto the navigator. void didPush() { } /// Called after install() when the route replaced another in the navigator. void didReplace(Route oldRoute) { } /// A request was made to pop this route. If the route can handle it /// internally (e.g. because it has its own stack of internal state) then /// return false, otherwise return true. Returning false will prevent the /// default behavior of NavigatorState.pop(). /// /// If this is called, the Navigator will not call dispose(). It is the /// responsibility of the Route to later call dispose(). bool didPop(T result) => true; /// Whether calling didPop() would return false. bool get willHandlePopInternally => false; /// The given route, which came after this one, has been popped off the /// navigator. void didPopNext(Route nextRoute) { } /// This route's next route has changed to the given new route. This is called /// on a route whenever the next route changes for any reason, except for /// cases when didPopNext() would be called, so long as it is in the history. /// nextRoute will be null if there's no next route. void didChangeNext(Route nextRoute) { } /// The route should remove its overlays and free any other resources. /// /// A call to didPop() implies that the Route should call dispose() itself, /// but it is possible for dispose() to be called directly (e.g. if the route /// is replaced, or if the navigator itself is disposed). void dispose() { } /// Whether this route is the top-most route on the navigator. bool get isCurrent { if (_navigator == null) return false; assert(_navigator._history.contains(this)); return _navigator._history.last == this; } } class RouteSettings { const RouteSettings({ this.name, this.mostValuableKeys, this.isInitialRoute: false }); final String name; final Set<Key> mostValuableKeys; final bool isInitialRoute; String toString() { String result = '"$name"'; if (mostValuableKeys != null && mostValuableKeys.isNotEmpty) { result += '; keys:'; for (Key key in mostValuableKeys) result += ' $key'; } return result; } } typedef Route RouteFactory(RouteSettings settings); typedef void NavigatorTransactionCallback(NavigatorTransaction transaction); class NavigatorObserver { /// The navigator that the observer is observing, if any. NavigatorState get navigator => _navigator; NavigatorState _navigator; void didPush(Route route, Route previousRoute) { } void didPop(Route route, Route previousRoute) { } } class Navigator extends StatefulComponent { Navigator({ Key key, this.initialRoute, this.onGenerateRoute, this.onUnknownRoute, this.observer }) : super(key: key) { assert(onGenerateRoute != null); } final String initialRoute; final RouteFactory onGenerateRoute; final RouteFactory onUnknownRoute; final NavigatorObserver observer; static const String defaultRouteName = '/'; static void pushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) { openTransaction(context, (NavigatorTransaction transaction) { transaction.pushNamed(routeName, mostValuableKeys: mostValuableKeys); }); } static void push(BuildContext context, Route route) { openTransaction(context, (NavigatorTransaction transaction) { transaction.push(route); }); } static bool pop(BuildContext context, [ dynamic result ]) { bool returnValue; openTransaction(context, (NavigatorTransaction transaction) { returnValue = transaction.pop(result); }); return returnValue; } static void popUntil(BuildContext context, Route targetRoute) { openTransaction(context, (NavigatorTransaction transaction) { transaction.popUntil(targetRoute); }); } static bool canPop(BuildContext context) { NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>()); return navigator != null && navigator.canPop(); } static void popAndPushNamed(BuildContext context, String routeName, { Set<Key> mostValuableKeys }) { openTransaction(context, (NavigatorTransaction transaction) { transaction.pop(); transaction.pushNamed(routeName, mostValuableKeys: mostValuableKeys); }); } static void openTransaction(BuildContext context, NavigatorTransactionCallback callback) { NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher<NavigatorState>()); navigator.openTransaction(callback); } NavigatorState createState() => new NavigatorState(); } class NavigatorState extends State<Navigator> { final GlobalKey<OverlayState> _overlayKey = new GlobalKey<OverlayState>(); final List<Route> _history = new List<Route>(); void initState() { super.initState(); assert(config.observer == null || config.observer.navigator == null); config.observer?._navigator = this; _push(config.onGenerateRoute(new RouteSettings( name: config.initialRoute ?? Navigator.defaultRouteName, isInitialRoute: true ))); } void didUpdateConfig(Navigator oldConfig) { if (oldConfig.observer != config.observer) { oldConfig.observer?._navigator = null; assert(config.observer == null || config.observer.navigator == null); config.observer?._navigator = this; } } void dispose() { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }); config.observer?._navigator = null; for (Route route in _history) { route.dispose(); route._navigator = null; } super.dispose(); assert(() { _debugLocked = false; return true; }); } // Used by Routes and NavigatorObservers OverlayState get overlay => _overlayKey.currentState; OverlayEntry get _currentOverlayEntry { for (Route route in _history.reversed) { if (route.overlayEntries.isNotEmpty) return route.overlayEntries.last; } return null; } bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends void _pushNamed(String name, { Set<Key> mostValuableKeys }) { assert(!_debugLocked); assert(name != null); RouteSettings settings = new RouteSettings( name: name, mostValuableKeys: mostValuableKeys ); Route route = config.onGenerateRoute(settings); if (route == null) { assert(config.onUnknownRoute != null); route = config.onUnknownRoute(settings); assert(route != null); } _push(route); } void _push(Route route) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }); assert(route != null); assert(route._navigator == null); setState(() { Route oldRoute = _history.isNotEmpty ? _history.last : null; route._navigator = this; route.install(_currentOverlayEntry); _history.add(route); route.didPush(); route.didChangeNext(null); if (oldRoute != null) oldRoute.didChangeNext(route); config.observer?.didPush(route, oldRoute); }); assert(() { _debugLocked = false; return true; }); } void _replace({ Route oldRoute, Route newRoute }) { assert(!_debugLocked); assert(oldRoute != null); assert(newRoute != null); if (oldRoute == newRoute) return; assert(() { _debugLocked = true; return true; }); assert(oldRoute._navigator == this); assert(newRoute._navigator == null); assert(oldRoute.overlayEntries.isNotEmpty); assert(newRoute.overlayEntries.isEmpty); assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last)); setState(() { int index = _history.indexOf(oldRoute); assert(index >= 0); newRoute._navigator = this; newRoute.install(oldRoute.overlayEntries.last); _history[index] = newRoute; newRoute.didReplace(oldRoute); if (index + 1 < _history.length) newRoute.didChangeNext(_history[index + 1]); else newRoute.didChangeNext(null); if (index > 0) _history[index - 1].didChangeNext(newRoute); oldRoute.dispose(); oldRoute._navigator = null; }); assert(() { _debugLocked = false; return true; }); } void _replaceRouteBefore({ Route anchorRoute, Route newRoute }) { assert(anchorRoute != null); assert(anchorRoute._navigator == this); assert(_history.indexOf(anchorRoute) > 0); _replace(oldRoute: _history[_history.indexOf(anchorRoute)-1], newRoute: newRoute); } void _removeRouteBefore(Route anchorRoute) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }); assert(anchorRoute._navigator == this); int index = _history.indexOf(anchorRoute) - 1; assert(index >= 0); Route targetRoute = _history[index]; assert(targetRoute._navigator == this); assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last)); setState(() { _history.removeAt(index); Route newRoute = index < _history.length ? _history[index] : null; if (index > 0) _history[index - 1].didChangeNext(newRoute); targetRoute.dispose(); targetRoute._navigator = null; }); assert(() { _debugLocked = false; return true; }); } bool _pop([dynamic result]) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }); Route route = _history.last; assert(route._navigator == this); bool debugPredictedWouldPop; assert(() { debugPredictedWouldPop = !route.willHandlePopInternally; return true; }); if (route.didPop(result)) { assert(debugPredictedWouldPop); if (_history.length > 1) { setState(() { // We use setState to guarantee that we'll rebuild, since the routes // can't do that for themselves, even if they have changed their own // state (e.g. ModalScope.isCurrent). _history.removeLast(); _history.last.didPopNext(route); config.observer?.didPop(route, _history.last); route._navigator = null; }); } else { assert(() { _debugLocked = false; return true; }); return false; } } else { assert(!debugPredictedWouldPop); } assert(() { _debugLocked = false; return true; }); return true; } void _popUntil(Route targetRoute) { assert(_history.contains(targetRoute)); while (!targetRoute.isCurrent) _pop(); } bool canPop() { assert(_history.length > 0); return _history.length > 1 || _history[0].willHandlePopInternally; } bool _hadTransaction = true; bool openTransaction(NavigatorTransactionCallback callback) { assert(callback != null); if (_hadTransaction) return false; _hadTransaction = true; NavigatorTransaction transaction = new NavigatorTransaction._(this); setState(() { callback(transaction); }); assert(() { transaction._debugClose(); return true; }); return true; } Widget build(BuildContext context) { assert(!_debugLocked); assert(_history.isNotEmpty); _hadTransaction = false; return new Overlay( key: _overlayKey, initialEntries: _history.first.overlayEntries ); } } class NavigatorTransaction { NavigatorTransaction._(this._navigator) { assert(_navigator != null); } NavigatorState _navigator; bool _debugOpen = true; /// Invokes the Navigator's onGenerateRoute callback to create a route with /// the given name, then calls [push()] with that route. void pushNamed(String name, { Set<Key> mostValuableKeys }) { assert(_debugOpen); _navigator._pushNamed(name, mostValuableKeys: mostValuableKeys); } /// Adds the given route to the Navigator's history, and transitions to it. /// The route will have didPush() and didChangeNext() called on it; the /// previous route, if any, will have didChangeNext() called on it; and the /// Navigator observer, if any, will have didPush() called on it. void push(Route route) { assert(_debugOpen); _navigator._push(route); } /// Replaces one given route with another. Calls install(), didReplace(), and /// didChangeNext() on the new route, then dispose() on the old route. The /// navigator is not informed of the replacement. /// /// The old route must have overlay entries, otherwise we won't know where to /// insert the entries of the new route. The old route must not be currently /// visible (i.e. a later route have overlay entries that are currently /// opaque), otherwise the replacement would have a jarring effect. /// /// It is safe to call this redundantly (replacing a route with itself). Such /// calls are ignored. void replace({ Route oldRoute, Route newRoute }) { assert(_debugOpen); _navigator._replace(oldRoute: oldRoute, newRoute: newRoute); } /// Like replace(), but affects the route before the given anchorRoute rather /// than the anchorRoute itself. /// /// If newRoute is already the route before anchorRoute, then the call is /// ignored. /// /// The conditions described for [replace()] apply; for instance, the route /// before anchorRoute must have overlay entries. void replaceRouteBefore({ Route anchorRoute, Route newRoute }) { assert(_debugOpen); _navigator._replaceRouteBefore(anchorRoute: anchorRoute, newRoute: newRoute); } /// Removes the route prior to the given anchorRoute, and calls didChangeNext /// on the route prior to that one, if any. The observer is not notified. void removeRouteBefore(Route anchorRoute) { assert(_debugOpen); _navigator._removeRouteBefore(anchorRoute); } /// Tries to removes the current route, calling its didPop() method. If that /// method returns false, then nothing else happens. Otherwise, the observer /// (if any) is notified using its didPop() method, and the previous route is /// notified using [Route.didChangeNext]. /// /// The type of the result argument, if provided, must match the type argument /// of the class of the current route. (In practice, this is usually /// "dynamic".) /// /// Returns true if a route was popped; returns false if there are no further /// previous routes. bool pop([dynamic result]) { assert(_debugOpen); return _navigator._pop(result); } /// Calls pop() repeatedly until the given route is the current route. /// If it is already the current route, nothing happens. void popUntil(Route targetRoute) { assert(_debugOpen); _navigator._popUntil(targetRoute); } void _debugClose() { assert(_debugOpen); _debugOpen = false; } }