Commit 24cab899 authored by Hixie's avatar Hixie

Tooltips

Introduces a new Tooltip class.
Adds support for tooltips to IconButton and Scaffold.
Adds some tooltips to various demos.

Also some tweaks to stack.dart that I made before I decided not to go
down a "CustomPositioned" route.
parent 28d4e52a
......@@ -73,7 +73,8 @@ class TabViewDemo extends StatelessComponent {
children: <Widget>[
new IconButton(
icon: "navigation/arrow_back",
onPressed: () { _handleArrowButtonPress(context, -1); }
onPressed: () { _handleArrowButtonPress(context, -1); },
tooltip: 'Back'
),
new Row(
children: _iconNames.map((String name) => _buildTabIndicator(context, name)).toList(),
......@@ -81,7 +82,8 @@ class TabViewDemo extends StatelessComponent {
),
new IconButton(
icon: "navigation/arrow_forward",
onPressed: () { _handleArrowButtonPress(context, 1); }
onPressed: () { _handleArrowButtonPress(context, 1); },
tooltip: 'Forward'
)
],
justifyContent: FlexJustifyContent.spaceBetween
......
......@@ -154,11 +154,13 @@ class StockHomeState extends State<StockHome> {
right: <Widget>[
new IconButton(
icon: "action/search",
onPressed: _handleSearchBegin
onPressed: _handleSearchBegin,
tooltip: 'Search'
),
new IconButton(
icon: "navigation/more_vert",
onPressed: _handleMenuShow
onPressed: _handleMenuShow,
tooltip: 'Show menu'
)
],
tabBar: new TabBar<StockHomeTab>(
......@@ -229,7 +231,8 @@ class StockHomeState extends State<StockHome> {
left: new IconButton(
icon: 'navigation/arrow_back',
colorFilter: new ColorFilter.mode(Theme.of(context).accentColor, ui.TransferMode.srcATop),
onPressed: _handleSearchEnd
onPressed: _handleSearchEnd,
tooltip: 'Back'
),
center: new Input(
key: searchFieldKey,
......
......@@ -53,6 +53,7 @@ export 'src/material/time_picker.dart';
export 'src/material/time_picker_dialog.dart';
export 'src/material/toggleable.dart';
export 'src/material/tool_bar.dart';
export 'src/material/tooltip.dart';
export 'src/material/typography.dart';
export 'widgets.dart';
......@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
import 'icon.dart';
import 'icon_theme_data.dart';
import 'ink_well.dart';
import 'tooltip.dart';
class IconButton extends StatelessComponent {
const IconButton({
......@@ -14,16 +15,18 @@ class IconButton extends StatelessComponent {
this.icon,
this.color,
this.colorFilter,
this.onPressed
this.onPressed,
this.tooltip
}) : super(key: key);
final String icon;
final IconThemeColor color;
final ColorFilter colorFilter;
final VoidCallback onPressed;
final String tooltip;
Widget build(BuildContext context) {
return new InkResponse(
Widget result = new InkResponse(
onTap: onPressed,
child: new Padding(
padding: const EdgeDims.all(8.0),
......@@ -34,10 +37,23 @@ class IconButton extends StatelessComponent {
)
)
);
if (tooltip != null) {
result = new Tooltip(
message: tooltip,
child: result
);
}
return result;
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$icon');
if (onPressed == null)
description.add('disabled');
if (color != null)
description.add('$color');
if (tooltip != null)
description.add('tooltip: "$tooltip"');
}
}
......@@ -331,14 +331,16 @@ class ScaffoldState extends State<Scaffold> {
if (config.drawer != null) {
left = new IconButton(
icon: 'navigation/menu',
onPressed: openDrawer
onPressed: openDrawer,
tooltip: 'Open navigation menu' // TODO(ianh): Figure out how to localize this string
);
} else {
_shouldShowBackArrow ??= Navigator.canPop(context);
if (_shouldShowBackArrow) {
left = new IconButton(
icon: 'navigation/arrow_back',
onPressed: () => Navigator.pop(context)
onPressed: () => Navigator.pop(context),
tooltip: 'Back' // TODO(ianh): Figure out how to localize this string
);
}
}
......
// 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 'dart:async';
import 'dart:math' as math;
import 'package:flutter/animation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'theme.dart';
const double _kDefaultTooltipBorderRadius = 2.0;
const double _kDefaultTooltipHeight = 32.0;
const EdgeDims _kDefaultTooltipPadding = const EdgeDims.symmetric(horizontal: 16.0);
const double _kDefaultVerticalTooltipOffset = 24.0;
const EdgeDims _kDefaultTooltipScreenEdgeMargin = const EdgeDims.all(10.0);
const Duration _kDefaultTooltipFadeDuration = const Duration(milliseconds: 200);
const Duration _kDefaultTooltipShowDuration = const Duration(seconds: 2);
class Tooltip extends StatefulComponent {
Tooltip({
Key key,
this.message,
this.backgroundColor,
this.textColor,
this.style,
this.opacity: 0.9,
this.borderRadius: _kDefaultTooltipBorderRadius,
this.height: _kDefaultTooltipHeight,
this.padding: _kDefaultTooltipPadding,
this.verticalOffset: _kDefaultVerticalTooltipOffset,
this.screenEdgeMargin: _kDefaultTooltipScreenEdgeMargin,
this.preferBelow: true,
this.fadeDuration: _kDefaultTooltipFadeDuration,
this.showDuration: _kDefaultTooltipShowDuration,
this.child
}) : super(key: key) {
assert(message != null);
assert(opacity != null);
assert(borderRadius != null);
assert(height != null);
assert(padding != null);
assert(verticalOffset != null);
assert(screenEdgeMargin != null);
assert(preferBelow != null);
assert(fadeDuration != null);
assert(showDuration != null);
}
final String message;
final Color backgroundColor;
final Color textColor;
final TextStyle style;
final double opacity;
final double borderRadius;
final double height;
final EdgeDims padding;
final double verticalOffset;
final EdgeDims screenEdgeMargin;
final bool preferBelow;
final Duration fadeDuration;
final Duration showDuration;
final Widget child;
_TooltipState createState() => new _TooltipState();
}
class _TooltipState extends State<Tooltip> {
Performance _performance;
OverlayEntry _entry;
Timer _timer;
void initState() {
super.initState();
_performance = new Performance(duration: config.fadeDuration)
..addStatusListener((PerformanceStatus status) {
switch (status) {
case PerformanceStatus.completed:
assert(_entry != null);
assert(_timer == null);
resetShowTimer();
break;
case PerformanceStatus.dismissed:
assert(_entry != null);
assert(_timer == null);
_entry.remove();
_entry = null;
break;
default:
break;
}
});
}
void didUpdateConfig(Tooltip oldConfig) {
super.didUpdateConfig(oldConfig);
if (config.fadeDuration != oldConfig.fadeDuration)
_performance.duration = config.fadeDuration;
if (_entry != null &&
(config.message != oldConfig.message ||
config.backgroundColor != oldConfig.backgroundColor ||
config.style != oldConfig.style ||
config.textColor != oldConfig.textColor ||
config.borderRadius != oldConfig.borderRadius ||
config.height != oldConfig.height ||
config.padding != oldConfig.padding ||
config.opacity != oldConfig.opacity ||
config.verticalOffset != oldConfig.verticalOffset ||
config.screenEdgeMargin != oldConfig.screenEdgeMargin ||
config.preferBelow != oldConfig.preferBelow))
_entry.markNeedsBuild();
}
void resetShowTimer() {
assert(_performance.status == PerformanceStatus.completed);
assert(_entry != null);
_timer = new Timer(config.showDuration, hideTooltip);
}
void showTooltip() {
if (_entry == null) {
RenderBox box = context.findRenderObject();
Point target = box.localToGlobal(box.size.center(Point.origin));
_entry = new OverlayEntry(builder: (BuildContext context) {
TextStyle textStyle = (config.style ?? Theme.of(context).text.body1).copyWith(color: config.textColor ?? Colors.white);
return new _TooltipOverlay(
message: config.message,
backgroundColor: config.backgroundColor ?? Colors.grey[700],
style: textStyle,
borderRadius: config.borderRadius,
height: config.height,
padding: config.padding,
opacity: config.opacity,
performance: _performance,
target: target,
verticalOffset: config.verticalOffset,
screenEdgeMargin: config.screenEdgeMargin,
preferBelow: config.preferBelow
);
});
Overlay.of(context).insert(_entry);
}
_timer?.cancel();
if (_performance.status != PerformanceStatus.completed) {
_timer = null;
_performance.forward();
} else {
resetShowTimer();
}
}
void hideTooltip() {
assert(_entry != null);
_timer?.cancel();
_timer = null;
_performance.reverse();
}
void deactivate() {
if (_entry != null)
hideTooltip();
super.deactivate();
}
Widget build(BuildContext context) {
assert(Overlay.of(context) != null);
return new GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: showTooltip,
child: config.child
);
}
}
class _TooltipPositionDelegate extends OneChildLayoutDelegate {
_TooltipPositionDelegate({
this.target,
this.verticalOffset,
this.screenEdgeMargin,
this.preferBelow
});
final Point target;
final double verticalOffset;
final EdgeDims screenEdgeMargin;
final bool preferBelow;
BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();
Offset getPositionForChild(Size size, Size childSize) {
// VERTICAL DIRECTION
final bool fitsBelow = target.y + verticalOffset + childSize.height <= size.height - screenEdgeMargin.bottom;
final bool fitsAbove = target.y - verticalOffset - childSize.height >= screenEdgeMargin.top;
final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow);
double y;
if (tooltipBelow)
y = math.min(target.y + verticalOffset, size.height - screenEdgeMargin.bottom);
else
y = math.max(target.y - verticalOffset - childSize.height, screenEdgeMargin.top);
// HORIZONTAL DIRECTION
double normalizedTargetX = target.x.clamp(screenEdgeMargin.left, size.width - screenEdgeMargin.right);
double x;
if (normalizedTargetX < screenEdgeMargin.left + childSize.width / 2.0) {
x = screenEdgeMargin.left;
} else if (normalizedTargetX > size.width - screenEdgeMargin.right - childSize.width / 2.0) {
x = size.width - screenEdgeMargin.right - childSize.width;
} else {
x = normalizedTargetX + childSize.width / 2.0;
}
return new Offset(x, y);
}
bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
return target != target
|| verticalOffset != verticalOffset
|| screenEdgeMargin != screenEdgeMargin
|| preferBelow != preferBelow;
}
}
class _TooltipOverlay extends StatelessComponent {
_TooltipOverlay({
Key key,
this.message,
this.backgroundColor,
this.style,
this.borderRadius,
this.height,
this.padding,
this.opacity,
this.performance,
this.target,
this.verticalOffset,
this.screenEdgeMargin,
this.preferBelow
}) : super(key: key);
final String message;
final Color backgroundColor;
final TextStyle style;
final double opacity;
final double borderRadius;
final double height;
final EdgeDims padding;
final PerformanceView performance;
final Point target;
final double verticalOffset;
final EdgeDims screenEdgeMargin;
final bool preferBelow;
Widget build(BuildContext context) {
return new Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
bottom: 0.0,
child: new IgnorePointer(
child: new CustomOneChildLayout(
delegate: new _TooltipPositionDelegate(
target: target,
verticalOffset: verticalOffset,
screenEdgeMargin: screenEdgeMargin,
preferBelow: preferBelow
),
child: new FadeTransition(
performance: performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, curve: Curves.ease),
child: new Opacity(
opacity: opacity,
child: new Container(
decoration: new BoxDecoration(
backgroundColor: backgroundColor,
borderRadius: borderRadius
),
height: height,
padding: padding,
child: new Center(
widthFactor: 1.0,
child: new Text(message, style: style)
)
)
)
)
)
)
);
}
}
......@@ -5,6 +5,8 @@
import 'box.dart';
import 'object.dart';
// For OneChildLayoutDelegate and RenderCustomOneChildLayoutBox, see shifted_box.dart
class MultiChildLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
/// An object representing the identity of this child.
Object id;
......
......@@ -14,6 +14,9 @@ import 'object.dart';
/// container, this class has no width and height members. To determine the
/// width or height of the rectangle, convert it to a [Rect] using [toRect()]
/// (passing the container's own Rect), and then examine that object.
///
/// If you create the RelativeRect with null values, the methods on
/// RelativeRect will not work usefully (or at all).
class RelativeRect {
/// Creates a RelativeRect with the given values.
......@@ -125,7 +128,7 @@ class RelativeRect {
int get hashCode => hashValues(left, top, right, bottom);
String toString() => "RelativeRect.fromLTRB(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)})";
String toString() => "RelativeRect.fromLTRB(${left?.toStringAsFixed(1)}, ${top?.toStringAsFixed(1)}, ${right?.toStringAsFixed(1)}, ${bottom?.toStringAsFixed(1)})";
}
/// Parent data for use with [RenderStack]
......@@ -155,10 +158,10 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
/// Get or set the current values in terms of a RelativeRect object.
RelativeRect get rect => new RelativeRect.fromLTRB(left, top, right, bottom);
void set rect(RelativeRect value) {
left = value.left;
top = value.top;
right = value.right;
bottom = value.bottom;
left = value.left;
}
void merge(StackParentData other) {
......@@ -185,7 +188,24 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
/// children in the stack.
bool get isPositioned => top != null || right != null || bottom != null || left != null || width != null || height != null;
String toString() => '${super.toString()}; top=$top; right=$right; bottom=$bottom; left=$left; width=$width; height=$height';
String toString() {
List<String> values = <String>[];
if (top != null)
values.add('top=$top');
if (right != null)
values.add('right=$right');
if (bottom != null)
values.add('bottom=$bottom');
if (left != null)
values.add('left=$left');
if (width != null)
values.add('width=$width');
if (height != null)
values.add('height=$height');
if (values.length == null)
return 'all null';
return values.join('; ');
}
}
abstract class RenderStackBase extends RenderBox
......
This diff is collapsed.
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