Commit 5462ddb9 authored by Jacob Richman's avatar Jacob Richman Committed by GitHub

Bare bones widget inspector support. (#10332)

Bare bones widget inspector support.

Toggle the widget inspector from the flutter tool by pressing 'i'.
When the widget inspector is select mode:
Pointer down to to inspect a widget.
Pointer click to finalize selection of a widget. You can now interact
with the application as you normally would but with the inspected widget
highlighted.
Click the inspect icon in bottom left corner of screen to reactivate
select mode.
parent 9c04aa15
......@@ -29,3 +29,4 @@ export 'src/painting/text_painter.dart';
export 'src/painting/text_span.dart';
export 'src/painting/text_style.dart';
export 'src/painting/transforms.dart';
export 'src/painting/utils.dart';
......@@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart';
import 'arc.dart';
import 'colors.dart';
import 'floating_action_button.dart';
import 'icons.dart';
import 'page.dart';
import 'theme.dart';
......@@ -373,6 +375,13 @@ class _MaterialAppState extends State<MaterialApp> {
checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
showSemanticsDebugger: widget.showSemanticsDebugger,
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
return new FloatingActionButton(
child: const Icon(Icons.search),
onPressed: onPressed,
mini: true,
);
},
)
);
......
......@@ -3,17 +3,16 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'feedback.dart';
import 'theme.dart';
import 'theme_data.dart';
const double _kScreenEdgeMargin = 10.0;
const Duration _kFadeDuration = const Duration(milliseconds: 200);
const Duration _kShowDuration = const Duration(milliseconds: 1500);
......@@ -198,15 +197,32 @@ class _TooltipState extends State<Tooltip> with SingleTickerProviderStateMixin {
}
}
/// A delegate for computing the layout of a tooltip to be displayed above or
/// bellow a target specified in the global coordinate system.
class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
/// Creates a delegate for computing the layout of a tooltip.
///
/// The arguments must not be null.
_TooltipPositionDelegate({
this.target,
this.verticalOffset,
this.preferBelow
});
@required this.target,
@required this.verticalOffset,
@required this.preferBelow,
}) : assert(target != null),
assert(verticalOffset != null),
assert(preferBelow != null);
/// The offset of the target the tooltip is positioned near in the global
/// coordinate system.
final Offset target;
/// The amount of vertical distance between the target and the displayed
/// tooltip.
final double verticalOffset;
/// Whether the tooltip defaults to being displayed below the widget.
///
/// If there is insufficient space to display the tooltip in the preferred
/// direction, the tooltip will be displayed in the opposite direction.
final bool preferBelow;
@override
......@@ -214,26 +230,13 @@ class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
@override
Offset getPositionForChild(Size size, Size childSize) {
// VERTICAL DIRECTION
final bool fitsBelow = target.dy + verticalOffset + childSize.height <= size.height - _kScreenEdgeMargin;
final bool fitsAbove = target.dy - verticalOffset - childSize.height >= _kScreenEdgeMargin;
final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow);
double y;
if (tooltipBelow)
y = math.min(target.dy + verticalOffset, size.height - _kScreenEdgeMargin);
else
y = math.max(target.dy - verticalOffset - childSize.height, _kScreenEdgeMargin);
// HORIZONTAL DIRECTION
final double normalizedTargetX = target.dx.clamp(_kScreenEdgeMargin, size.width - _kScreenEdgeMargin);
double x;
if (normalizedTargetX < _kScreenEdgeMargin + childSize.width / 2.0) {
x = _kScreenEdgeMargin;
} else if (normalizedTargetX > size.width - _kScreenEdgeMargin - childSize.width / 2.0) {
x = size.width - _kScreenEdgeMargin - childSize.width;
} else {
x = normalizedTargetX - childSize.width / 2.0;
}
return new Offset(x, y);
return positionDependentBox(
size: size,
childSize: childSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
);
}
@override
......
// Copyright 2017 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:math' as math;
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
const double _kScreenEdgeMargin = 10.0;
/// Position a box either above or bellow a target box specified in the global
/// coordinate system.
///
/// The target box is specified by [size] and [target] and the box being
/// positioned is specified by [childSize]. [verticalOffset] is the amount of
/// vertical distance between the boxes.
///
/// Used by [Tooltip] to position a tooltip relative to its parent.
///
/// The arguments must not be null.
Offset positionDependentBox({
@required Size size,
@required Size childSize,
@required Offset target,
@required double verticalOffset,
@required bool preferBelow,
}) {
assert(size != null);
assert(childSize != null);
assert(target != null);
assert(verticalOffset != null);
assert(preferBelow != null);
// VERTICAL DIRECTION
final bool fitsBelow = target.dy + verticalOffset + childSize.height <= size.height - _kScreenEdgeMargin;
final bool fitsAbove = target.dy - verticalOffset - childSize.height >= _kScreenEdgeMargin;
final bool tooltipBelow = preferBelow ? fitsBelow || !fitsAbove : !(fitsAbove || !fitsBelow);
double y;
if (tooltipBelow)
y = math.min(target.dy + verticalOffset, size.height - _kScreenEdgeMargin);
else
y = math.max(target.dy - verticalOffset - childSize.height, _kScreenEdgeMargin);
// HORIZONTAL DIRECTION
final double normalizedTargetX = target.dx.clamp(_kScreenEdgeMargin, size.width - _kScreenEdgeMargin);
double x;
if (normalizedTargetX < _kScreenEdgeMargin + childSize.width / 2.0) {
x = _kScreenEdgeMargin;
} else if (normalizedTargetX > size.width - _kScreenEdgeMargin - childSize.width / 2.0) {
x = size.width - _kScreenEdgeMargin - childSize.width;
} else {
x = normalizedTargetX - childSize.width / 2.0;
}
return new Offset(x, y);
}
......@@ -2817,7 +2817,6 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
description.add(new FlagProperty('isSemanticBoundary', value: isSemanticBoundary, ifTrue: 'semantic boundary'));
}
@protected
@override
List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[];
......
......@@ -20,6 +20,7 @@ import 'performance_overlay.dart';
import 'semantics_debugger.dart';
import 'text.dart';
import 'title.dart';
import 'widget_inspector.dart';
/// Signature for a function that is called when the operating system changes the current locale.
///
......@@ -58,7 +59,9 @@ class WidgetsApp extends StatefulWidget {
this.checkerboardRasterCacheImages: false,
this.checkerboardOffscreenLayers: false,
this.showSemanticsDebugger: false,
this.debugShowCheckedModeBanner: true
this.debugShowWidgetInspector: false,
this.debugShowCheckedModeBanner: true,
this.inspectorSelectButtonBuilder,
}) : assert(onGenerateRoute != null),
assert(color != null),
assert(navigatorObservers != null),
......@@ -67,6 +70,7 @@ class WidgetsApp extends StatefulWidget {
assert(checkerboardOffscreenLayers != null),
assert(showSemanticsDebugger != null),
assert(debugShowCheckedModeBanner != null),
assert(debugShowWidgetInspector != null),
super(key: key);
/// A one-line description of this app for use in the window manager.
......@@ -148,6 +152,21 @@ class WidgetsApp extends StatefulWidget {
/// reported by the framework.
final bool showSemanticsDebugger;
/// Turns on an overlay that enables inspecting the widget tree.
///
/// The inspector is only available in checked mode as it depends on
/// [RenderObject.debugDescribeChildren] which should not be called outside of
/// checked mode.
final bool debugShowWidgetInspector;
/// Builds the widget the [WidgetInspector] uses to switch between view and
/// inspect modes.
///
/// This lets [MaterialApp] to use a material button to toggle the inspector
/// select mode without requiring [WidgetInspector] to depend on the the
/// material package.
final InspectorSelectButtonBuilder inspectorSelectButtonBuilder;
/// Turns on a "SLOW MODE" little banner in checked mode to indicate
/// that the app is in checked mode. This is on by default (in
/// checked mode), to turn it off, set the constructor argument to
......@@ -168,12 +187,22 @@ class WidgetsApp extends StatefulWidget {
/// If true, forces the performance overlay to be visible in all instances.
///
/// Used by `showPerformanceOverlay` observatory extension.
/// Used by the `showPerformanceOverlay` observatory extension.
static bool showPerformanceOverlayOverride = false;
/// If true, forces the widget inspector to be visible.
///
/// Used by the `debugShowWidgetInspector` debugging extension.
///
/// The inspector allows you to select a location on your device or emulator
/// and view what widgets and render objects associated with it. An outline of
/// the selected widget and some summary information is shown on device and
/// more detailed information is shown in the IDE or Observatory.
static bool debugShowWidgetInspectorOverride = false;
/// If false, prevents the debug banner from being visible.
///
/// Used by `debugAllowBanner` observatory extension.
/// Used by the `debugAllowBanner` observatory extension.
///
/// This is how `flutter run` turns off the banner when you take a screen shot
/// with "s".
......@@ -303,17 +332,24 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
}
if (widget.showSemanticsDebugger) {
result = new SemanticsDebugger(
child: result
child: result,
);
}
assert(() {
if (widget.debugShowWidgetInspector || WidgetsApp.debugShowWidgetInspectorOverride) {
result = new WidgetInspector(
child: result,
selectButtonBuilder: widget.inspectorSelectButtonBuilder,
);
}
if (widget.debugShowCheckedModeBanner && WidgetsApp.debugAllowBannerOverride) {
result = new CheckedModeBanner(
child: result
child: result,
);
}
return true;
});
return result;
}
......
......@@ -219,6 +219,17 @@ abstract class WidgetsBinding extends BindingBase with GestureBinding, RendererB
return _forceRebuild();
}
);
registerBoolServiceExtension(
name: 'debugWidgetInspector',
getter: () async => WidgetsApp.debugShowWidgetInspectorOverride,
setter: (bool value) {
if (WidgetsApp.debugShowWidgetInspectorOverride == value)
return new Future<Null>.value();
WidgetsApp.debugShowWidgetInspectorOverride = value;
return _forceRebuild();
}
);
}
Future<Null> _forceRebuild() {
......
// Copyright 2017 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:collection';
import 'dart:developer' as developer;
import 'dart:math' as math;
import 'dart:ui' as ui show window, Picture, SceneBuilder, PictureRecorder;
import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'binding.dart';
import 'framework.dart';
import 'gesture_detector.dart';
/// Signature for the builder callback used by
/// [WidgetInspector.selectButtonBuilder].
typedef Widget InspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed);
/// A widget that enables inspecting the child widget's structure.
///
/// Select a location on your device or emulator and view what widgets and
/// render object that best matches the location. An outline of the selected
/// widget and terse summary information is shown on device with detailed
/// information is shown in the observatory or in IntelliJ when using the
/// Flutter Plugin.
///
/// The inspector has a select mode and a view mode.
///
/// In the select mode, tapping the device selects the widget that best matches
/// the location of the touch and switches to view mode. Dragging a finger on
/// the device selects the widget under the drag location but does not switch
/// modes. Touching the very edge of the bounding box of a widget triggers
/// selecting the widget even if another widget that also overlaps that
/// location would otherwise have priority.
///
/// In the view mode, the previously selected widget is outlined, however,
/// touching the device has the same effect it would have if the inspector
/// wasn't present. This allows interacting with the application and viewing how
/// the selected widget changes position. Clicking on the select icon in the
/// bottom left corner of the application switches back to select mode.
class WidgetInspector extends StatefulWidget {
/// Creates a widget that enables inspection for the child.
///
/// The [child] argument must not be null.
const WidgetInspector({
Key key,
@required this.child,
@required this.selectButtonBuilder,
}) : assert(child != null),
super(key: key);
/// The widget that is being inspected.
final Widget child;
/// A builder that is called to create the select button.
///
/// The `onPressed` callback passed as an argument to the builder should be
/// hooked up to the returned widget.
final InspectorSelectButtonBuilder selectButtonBuilder;
@override
_WidgetInspectorState createState() => new _WidgetInspectorState();
}
class _WidgetInspectorState extends State<WidgetInspector>
with WidgetsBindingObserver {
Offset _lastPointerLocation;
final InspectorSelection selection = new InspectorSelection();
/// Whether the inspector is in select mode.
///
/// In select mode, pointer interactions trigger widget selection instead of
/// normal interactions. Otherwise the previously selected widget is
/// highlighted but the application can be interacted with normally.
bool isSelectMode = true;
final GlobalKey _ignorePointerKey = new GlobalKey();
/// Distance from the edge of of the bounding box for an element to consider
/// as selecting the edge of the bounding box.
static const double _kEdgeHitMargin = 2.0;
bool _hitTestHelper(
List<RenderObject> hits,
List<RenderObject> edgeHits,
Offset position,
RenderObject object,
Matrix4 transform,
) {
bool hit = false;
final Matrix4 inverse = new Matrix4.inverted(transform);
final Offset localPosition = MatrixUtils.transformPoint(inverse, position);
final List<DiagnosticsNode> children = object.debugDescribeChildren();
for (int i = children.length - 1; i >= 0; i -= 1) {
final DiagnosticsNode diagnostics = children[i];
if (diagnostics.style == DiagnosticsTreeStyle.offstage ||
diagnostics.value is! RenderObject)
continue;
final RenderObject child = diagnostics.value;
final Rect paintClip = object.describeApproximatePaintClip(child);
if (paintClip != null && !paintClip.contains(localPosition))
continue;
final Matrix4 childTransform = transform.clone();
object.applyPaintTransform(child, childTransform);
if (_hitTestHelper(hits, edgeHits, position, child, childTransform))
hit = true;
}
final Rect bounds = object.semanticBounds;
if (bounds.contains(localPosition)) {
hit = true;
// Hits that occur on the edge of the bounding box of an object are
// given priority to provide a way to select objects that would
// otherwise be hard to select.
if (!bounds.deflate(_kEdgeHitMargin).contains(localPosition))
edgeHits.add(object);
}
if (hit)
hits.add(object);
return hit;
}
/// Returns the list of render objects located at the given position ordered
/// by priority.
///
/// All render objects that are not offstage that match the location are
/// included in the list of matches. Priority is given to matches that occur
/// on the edge of a render object's bounding box and to matches found by
/// [RenderBox.hitTest].
List<RenderObject> hitTest(Offset position, RenderObject root) {
final List<RenderObject> regularHits = <RenderObject>[];
final List<RenderObject> edgeHits = <RenderObject>[];
_hitTestHelper(regularHits, edgeHits, position, root, root.getTransformTo(null));
// Order matches by the size of the hit area.
double _area(RenderObject object) {
final Size size = object.semanticBounds?.size;
return size == null ? double.MAX_FINITE : size.width * size.height;
}
regularHits.sort((RenderObject a, RenderObject b) => _area(a).compareTo(_area(b)));
final Set<RenderObject> hits = new LinkedHashSet<RenderObject>();
hits..addAll(edgeHits)..addAll(regularHits);
return hits.toList();
}
void _inspectAt(Offset position) {
if (!isSelectMode)
return;
final RenderIgnorePointer ignorePointer = _ignorePointerKey.currentContext.findRenderObject();
final RenderObject userRender = ignorePointer.child;
final List<RenderObject> selected = hitTest(position, userRender);
setState(() {
selection.candidates = selected;
});
}
void _handlePanDown(DragDownDetails event) {
_lastPointerLocation = event.globalPosition;
_inspectAt(event.globalPosition);
}
void _handlePanUpdate(DragUpdateDetails event) {
_lastPointerLocation = event.globalPosition;
_inspectAt(event.globalPosition);
}
void _handlePanEnd(DragEndDetails details) {
// If the pan ends on the edge of the window assume that it indicates the
// pointer is being dragged off the edge of the display not a regular touch
// on the edge of the display. If the pointer is being dragged off the edge
// of the display we do not want to select anything. A user can still select
// a widget that is only at the exact screen margin by tapping.
final Rect bounds = (Offset.zero & (ui.window.physicalSize / ui.window.devicePixelRatio)).deflate(_kOffScreenMargin);
if (!bounds.contains(_lastPointerLocation)) {
setState(() {
selection.clear();
});
}
}
void _handleTap() {
if (!isSelectMode)
return;
if (_lastPointerLocation != null) {
_inspectAt(_lastPointerLocation);
if (selection != null) {
// Notify debuggers to open an inspector on the object.
developer.inspect(selection.current);
print(selection.current.toStringDeep());
}
}
setState(() {
// Only exit select mode if there is a button to return to select mode.
if (widget.selectButtonBuilder != null)
isSelectMode = false;
});
}
void _handleEnableSelect() {
setState(() {
isSelectMode = true;
});
}
@override
Widget build(BuildContext context) {
final List<Widget> children = <Widget>[];
children.add(new GestureDetector(
onTap: _handleTap,
onPanDown: _handlePanDown,
onPanEnd: _handlePanEnd,
onPanUpdate: _handlePanUpdate,
behavior: HitTestBehavior.opaque,
excludeFromSemantics: true,
child: new IgnorePointer(
ignoring: isSelectMode,
key: _ignorePointerKey,
ignoringSemantics: false,
child: widget.child,
),
));
if (!isSelectMode && widget.selectButtonBuilder != null) {
children.add(new Positioned(
left: _kInspectButtonMargin,
bottom: _kInspectButtonMargin,
child: widget.selectButtonBuilder(context, _handleEnableSelect)
));
}
children.add(new _InspectorOverlay(selection: selection));
return new Stack(children: children);
}
}
/// Mutable selection state of the inspector.
class InspectorSelection {
/// Render objects that are candidates to be selected.
///
/// Tools may wish to iterate through the list of candidates
List<RenderObject> get candidates => _candidates;
set candidates(List<RenderObject> value) {
_candidates = value;
index = 0;
}
List<RenderObject> _candidates = <RenderObject>[];
/// Index within the list of candidates that is currently selected.
int index = 0;
void clear() {
_candidates = <RenderObject>[];
index = 0;
}
RenderObject get current {
return candidates != null && index < candidates.length ? candidates[index] : null;
}
bool get active => current != null && current.attached;
}
class _InspectorOverlay extends LeafRenderObjectWidget {
const _InspectorOverlay({
Key key,
@required this.selection,
}) : super(key: key);
final InspectorSelection selection;
@override
_RenderInspectorOverlay createRenderObject(BuildContext context) {
return new _RenderInspectorOverlay(selection: selection);
}
@override
void updateRenderObject(BuildContext context, _RenderInspectorOverlay renderObject) {
renderObject.selection = selection;
}
}
class _RenderInspectorOverlay extends RenderBox {
/// The arguments must not be null.
_RenderInspectorOverlay({ @required InspectorSelection selection }) : _selection = selection, assert(selection != null);
InspectorSelection get selection => _selection;
InspectorSelection _selection;
set selection(InspectorSelection value) {
if (value != _selection) {
_selection = value;
}
markNeedsPaint();
}
@override
bool get sizedByParent => true;
@override
bool get alwaysNeedsCompositing => true;
@override
void performResize() {
size = constraints.constrain(new Size(double.INFINITY, double.INFINITY));
}
@override
void paint(PaintingContext context, Offset offset) {
assert(needsCompositing);
context.addLayer(new _InspectorOverlayLayer(
overlayRect: new Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
selection: selection,
));
}
}
class _TransformedRect {
_TransformedRect(RenderObject object) :
rect = object.semanticBounds,
transform = object.getTransformTo(null);
final Rect rect;
final Matrix4 transform;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final _TransformedRect typedOther = other;
return rect == typedOther.rect && transform == typedOther.transform;
}
@override
int get hashCode => hashValues(rect, transform);
}
/// State describing how the inspector overlay should be rendered.
///
/// The equality operator can be used to determine whether the overlay needs to
/// be rendered again.
class _InspectorOverlayRenderState {
_InspectorOverlayRenderState({
@required this.overlayRect,
@required this.selected,
@required this.candidates,
@required this.tooltip,
});
final Rect overlayRect;
final _TransformedRect selected;
final List<_TransformedRect> candidates;
final String tooltip;
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType)
return false;
final _InspectorOverlayRenderState typedOther = other;
return overlayRect == typedOther.overlayRect
&& selected == typedOther.selected
&& listEquals<_TransformedRect>(candidates, typedOther.candidates)
&& tooltip == typedOther.tooltip;
}
@override
int get hashCode => hashValues(overlayRect, selected, hashList(candidates), tooltip);
}
final int _kMaxTooltipLines = 5;
final Color _kTooltipBackgroundColor = const Color.fromARGB(230, 60, 60, 60);
final Color _kHighlightedRenderObjectFillColor = const Color.fromARGB(128, 128, 128, 255);
final Color _kHighlightedRenderObjectBorderColor = const Color.fromARGB(128, 64, 64, 128);
/// A layer that outlines the selected [RenderObject] and candidate render
/// objects that also match the last pointer location.
///
/// This approach is horrific for performance and is only used here because this
/// is limited to debug mode. Do not duplicate the logic in production code.
class _InspectorOverlayLayer extends Layer {
/// Creates a layer that displays the inspector overlay.
_InspectorOverlayLayer({
@required this.overlayRect,
@required this.selection,
}) : assert(overlayRect != null), assert(selection != null) {
bool inDebugMode = false;
assert(() {
inDebugMode = true;
return true;
});
if (inDebugMode == false) {
throw new FlutterError(
'The inspector should never be used in production mode due to the '
'negative performance impact.'
);
}
}
InspectorSelection selection;
/// The rectangle in this layer's coordinate system that the overlay should
/// occupy.
///
/// The scene must be explicitly recomposited after this property is changed
/// (as described at [Layer]).
final Rect overlayRect;
_InspectorOverlayRenderState _lastState;
/// Picture generated from _lastState.
ui.Picture _picture;
TextPainter _textPainter;
double _textPainterMaxWidth;
@override
void addToScene(ui.SceneBuilder builder, Offset layerOffset) {
if (!selection.active)
return;
final RenderObject selected = selection.current;
final List<_TransformedRect> candidates = <_TransformedRect>[];
for (RenderObject candidate in selection.candidates) {
if (candidate == selected || !candidate.attached)
continue;
candidates.add(new _TransformedRect(candidate));
}
final _InspectorOverlayRenderState state = new _InspectorOverlayRenderState(
overlayRect: overlayRect,
selected: new _TransformedRect(selected),
tooltip: selected.toString(),
candidates: candidates,
);
if (state != _lastState) {
_lastState = state;
_picture = _buildPicture(state);
}
builder.addPicture(layerOffset, _picture);
}
ui.Picture _buildPicture(_InspectorOverlayRenderState state) {
final ui.PictureRecorder recorder = new ui.PictureRecorder();
final Canvas canvas = new Canvas(recorder, state.overlayRect);
final Size size = state.overlayRect.size;
final Paint fillPaint = new Paint()
..style = PaintingStyle.fill
..color = _kHighlightedRenderObjectFillColor;
final Paint borderPaint = new Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = _kHighlightedRenderObjectBorderColor;
// Highlight the selected renderObject.
final Rect selectedPaintRect = state.selected.rect.deflate(0.5);
canvas
..save()
..transform(state.selected.transform.storage)
..drawRect(selectedPaintRect, fillPaint)
..drawRect(selectedPaintRect, borderPaint)
..restore();
// Show all other candidate possibly selected elements. This helps selecting
// render objects by selecting the edge of the bounding box shows all
// elements the user could toggle the selection between.
for (_TransformedRect transformedRect in state.candidates) {
canvas
..save()
..transform(transformedRect.transform.storage)
..drawRect(transformedRect.rect.deflate(0.5), borderPaint)
..restore();
}
final Rect targetRect = MatrixUtils.transformRect(
state.selected.transform, state.selected.rect);
final Offset target = new Offset(targetRect.left, targetRect.center.dy);
final double offsetFromWidget = 9.0;
final double verticalOffset = (targetRect.height) / 2 + offsetFromWidget;
_paintDescription(canvas, state.tooltip, target, verticalOffset, size, targetRect);
// TODO(jacobr): provide an option to perform a debug paint of just the
// selected widget.
return recorder.endRecording();
}
void _paintDescription(Canvas canvas, String message, Offset target,
double verticalOffset, Size size, Rect targetRect) {
canvas.save();
final double maxWidth = size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding);
if (_textPainter == null || _textPainter.text.text != message || _textPainterMaxWidth != maxWidth) {
_textPainterMaxWidth = maxWidth;
_textPainter = new TextPainter()
..maxLines = _kMaxTooltipLines
..ellipsis = '...'
..text = new TextSpan(style: _messageStyle, text: message)
..layout(maxWidth: maxWidth);
}
final Size tooltipSize = _textPainter.size + const Offset(_kTooltipPadding * 2, _kTooltipPadding * 2);
final Offset tipOffset = positionDependentBox(
size: size,
childSize: tooltipSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: false,
);
final Paint tooltipBackground = new Paint()
..style = PaintingStyle.fill
..color = _kTooltipBackgroundColor;
canvas.drawRect(
new Rect.fromPoints(
tipOffset,
tipOffset.translate(tooltipSize.width, tooltipSize.height),
),
tooltipBackground,
);
double wedgeY = tipOffset.dy;
final bool tooltipBelow = tipOffset.dy > target.dy;
if (!tooltipBelow)
wedgeY += tooltipSize.height;
final double wedgeSize = _kTooltipPadding * 2;
double wedgeX = math.max(tipOffset.dx, target.dx) + wedgeSize * 2;
wedgeX = math.min(wedgeX, tipOffset.dx + tooltipSize.width - wedgeSize * 2);
final List<Offset> wedge = <Offset>[
new Offset(wedgeX - wedgeSize, wedgeY),
new Offset(wedgeX + wedgeSize, wedgeY),
new Offset(wedgeX, wedgeY + (tooltipBelow ? -wedgeSize : wedgeSize)),
];
canvas.drawPath(new Path()..addPolygon(wedge, true,), tooltipBackground);
_textPainter.paint(canvas, tipOffset + const Offset(_kTooltipPadding, _kTooltipPadding));
canvas.restore();
}
}
const double _kScreenEdgeMargin = 10.0;
const double _kTooltipPadding = 5.0;
const double _kInspectButtonMargin = 10.0;
/// Interpret pointer up events within with this margin as indicating the
/// pointer is moving off the device.
const double _kOffScreenMargin = 1.0;
const TextStyle _messageStyle = const TextStyle(
color: const Color(0xFFFFFFFF),
fontSize: 10.0,
height: 1.2,
);
......@@ -91,4 +91,5 @@ export 'src/widgets/title.dart';
export 'src/widgets/transitions.dart';
export 'src/widgets/unique_widget.dart';
export 'src/widgets/viewport.dart';
export 'src/widgets/widget_inspector.dart';
export 'src/widgets/will_pop_scope.dart';
......@@ -433,6 +433,29 @@ void main() {
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - debugWidgetInspector', () async {
Map<String, String> result;
expect(binding.frameScheduled, isFalse);
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
result = await binding.testExtension('debugWidgetInspector', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
result = await binding.testExtension('debugWidgetInspector', <String, String>{ 'enabled': 'true' });
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, true);
result = await binding.testExtension('debugWidgetInspector', <String, String>{});
expect(result, <String, String>{ 'enabled': 'true' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, true);
result = await binding.testExtension('debugWidgetInspector', <String, String>{ 'enabled': 'false' });
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
result = await binding.testExtension('debugWidgetInspector', <String, String>{});
expect(result, <String, String>{ 'enabled': 'false' });
expect(WidgetsApp.debugShowWidgetInspectorOverride, false);
expect(binding.frameScheduled, isFalse);
});
test('Service extensions - timeDilation', () async {
Map<String, String> result;
......@@ -459,7 +482,7 @@ void main() {
test('Service extensions - posttest', () async {
// If you add a service extension... TEST IT! :-)
// ...then increment this number.
expect(binding.extensions.length, 15);
expect(binding.extensions.length, 16);
expect(console, isEmpty);
debugPrint = debugPrintThrottled;
......
// 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:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('WidgetInspector smoke test', (WidgetTester tester) async {
// This is a smoke test to verify that adding the inspector doesn't crash.
await tester.pumpWidget(
new Stack(
children: <Widget>[
const Text('a'),
const Text('b'),
const Text('c'),
],
),
);
await tester.pumpWidget(
new WidgetInspector(
selectButtonBuilder: null,
child: new Stack(
children: <Widget>[
const Text('a'),
const Text('b'),
const Text('c'),
],
),
),
);
expect(true, isTrue); // Expect that we reach here without crashing.
});
testWidgets('WidgetInspector interaction test', (WidgetTester tester) async {
final List<String> log = <String>[];
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey topButtonKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
await tester.pumpWidget(
new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new Material(
child: new ListView(
children: <Widget>[
new RaisedButton(
key: topButtonKey,
onPressed: () {
log.add('top');
},
child: const Text('TOP'),
),
new RaisedButton(
onPressed: () {
log.add('bottom');
},
child: const Text('BOTTOM'),
),
],
),
),
),
);
expect(getInspectorState().selection.current, isNull);
await tester.tap(find.text('TOP'));
await tester.pump();
// Tap intercepted by the inspector
expect(log, equals(<String>[]));
final InspectorSelection selection = getInspectorState().selection;
expect(paragraphText(selection.current), equals('TOP'));
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject;
expect(selection.candidates.contains(topButton), isTrue);
await tester.tap(find.text('TOP'));
expect(log, equals(<String>['top']));
log.clear();
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>['bottom']));
log.clear();
// Ensure the inspector selection has not changed to bottom.
expect(paragraphText(getInspectorState().selection.current), equals('TOP'));
await tester.tap(find.byKey(selectButtonKey));
await tester.pump();
// We are now back in select mode so tapping the bottom button will have
// not trigger a click but will cause it to be selected.
await tester.tap(find.text('BOTTOM'));
expect(log, equals(<String>[]));
log.clear();
expect(paragraphText(getInspectorState().selection.current), equals('BOTTOM'));
});
testWidgets('WidgetInspector scroll test', (WidgetTester tester) async {
final Key childKey = new UniqueKey();
final GlobalKey selectButtonKey = new GlobalKey();
final GlobalKey inspectorKey = new GlobalKey();
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
return new Material(child: new RaisedButton(onPressed: onPressed, key: selectButtonKey));
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
await tester.pumpWidget(
new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: selectButtonBuilder,
child: new ListView(
children: <Widget>[
new Container(
key: childKey,
height: 5000.0,
),
],
),
),
);
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
// Fling does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.fling(find.byType(ListView), const Offset(200.0, 0.0), 200.0);
await tester.pump();
// Fling still does nothing as are in inspect mode.
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
await tester.tap(find.byType(ListView));
await tester.pump();
expect(getInspectorState().selection.current, isNotNull);
// Now out of inspect mode due to the click.
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(-200.0));
await tester.fling(find.byType(ListView), const Offset(0.0, 200.0), 200.0);
await tester.pump();
expect(tester.getTopLeft(find.byKey(childKey)).dy, equals(0.0));
});
testWidgets('WidgetInspector long press', (WidgetTester tester) async {
bool didLongPress = false;
await tester.pumpWidget(
new WidgetInspector(
selectButtonBuilder: null,
child: new GestureDetector(
onLongPress: () {
expect(didLongPress, isFalse);
didLongPress = true;
},
child: const Text('target'),
),
),
);
await tester.longPress(find.text('target'));
// The inspector will swallow the long press.
expect(didLongPress, isFalse);
});
testWidgets('WidgetInspector offstage', (WidgetTester tester) async {
final GlobalKey inspectorKey = new GlobalKey();
final GlobalKey clickTarget = new GlobalKey();
Widget createSubtree({ double width, Key key }) {
return new Stack(
children: <Widget>[
new Positioned(
key: key,
left: 0.0,
top: 0.0,
width: width,
height: 100.0,
child: new Text(width.toString()),
),
]
);
}
await tester.pumpWidget(
new WidgetInspector(
key: inspectorKey,
selectButtonBuilder: null,
child: new Overlay(
initialEntries: <OverlayEntry>[
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 94.0),
),
new OverlayEntry(
opaque: true,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 95.0),
),
new OverlayEntry(
opaque: false,
maintainState: true,
builder: (BuildContext _) => createSubtree(width: 96.0, key: clickTarget),
),
],
),
),
);
await tester.longPress(find.byKey(clickTarget));
// State type is private, hence using dynamic.
final dynamic inspectorState = inspectorKey.currentState;
// The object with width 95.0 wins over the object with width 94.0 because
// the subtree with width 94.0 is offstage.
expect(inspectorState.selection.current.semanticBounds.width, equals(95.0));
// Exactly 2 out of the 3 text elements should be in the candidate list of
// objects to select as only 2 are onstage.
expect(inspectorState.selection.candidates.where((RenderObject object) => object is RenderParagraph).length, equals(2));
});
}
......@@ -160,6 +160,11 @@ class FlutterDevice {
await view.uiIsolate.flutterTogglePerformanceOverlayOverride();
}
Future<Null> toggleWidgetInspector() async {
for (FlutterView view in views)
await view.uiIsolate.flutterToggleWidgetInspector();
}
Future<String> togglePlatform({ String from }) async {
String to;
switch (from) {
......@@ -446,6 +451,12 @@ abstract class ResidentRunner {
await device.debugTogglePerformanceOverlayOverride();
}
Future<Null> _debugToggleWidgetInspector() async {
await refreshViews();
for (FlutterDevice device in flutterDevices)
await device.toggleWidgetInspector();
}
Future<Null> _screenshot(FlutterDevice device) async {
final Status status = logger.startProgress('Taking screenshot for ${device.device.name}...');
final File outputFile = getUniqueFile(fs.currentDirectory, 'flutter', 'png');
......@@ -609,6 +620,10 @@ abstract class ResidentRunner {
} else if (character == 'P') {
if (supportsServiceProtocol) {
await _debugTogglePerformanceOverlayOverride();
}
} else if (lower == 'i') {
if (supportsServiceProtocol) {
await _debugToggleWidgetInspector();
return true;
}
} else if (character == 's') {
......@@ -731,6 +746,7 @@ abstract class ResidentRunner {
printStatus('To dump the rendering tree of the app (debugDumpRenderTree), press "t".');
if (isRunningDebug) {
printStatus('For layers (debugDumpLayerTree), use "L"; accessibility (debugDumpSemantics), "S".');
printStatus('To toggle the widget inspector (WidgetsApp.showWidgetInspectorOverride), press "i".');
printStatus('To toggle the display of construction lines (debugPaintSizeEnabled), press "p".');
printStatus('To simulate different operating systems, (defaultTargetPlatform), press "o".');
} else {
......
......@@ -1090,6 +1090,8 @@ class Isolate extends ServiceObjectOwner {
Future<Map<String, dynamic>> flutterTogglePerformanceOverlayOverride() => _flutterToggle('showPerformanceOverlay');
Future<Map<String, dynamic>> flutterToggleWidgetInspector() => _flutterToggle('debugWidgetInspector');
Future<Null> flutterDebugAllowBanner(bool show) async {
await invokeFlutterExtensionRpcRaw(
'ext.flutter.debugAllowBanner',
......
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