On-device Widget Inspector button exits widget selection (#158219)
Fixes https://github.com/flutter/devtools/issues/8155 Previously after enabling Widget Selection mode from DevTools and selecting a widget to inspect, a user would then have to click the on-device "Select widget" button before being able to select another widget. This was very confusing to users; we got multiple comments on our latest DevTools Survey that widget selection mode only worked the first time and was broken on subsequent selections. Now, once "Select widget mode" is enabled from DevTools, any subsequent click is treated as a selection until the user exits from select widget mode either via DevTools or via the Exit Selection mode button. The user can re-position the Exit Selection button to either the left or the right of their device (this way they can select a widget beneath it).  Note: Previously this button was behind any widget selection overlays. This PR also updates the order of the `Stack` so that exit selection button is on top.
This commit is contained in:
parent
4e03220a86
commit
a051be8585
@ -534,18 +534,51 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
];
|
||||
}
|
||||
|
||||
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||
return CupertinoButton.filled(
|
||||
Widget _exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return CupertinoButton(
|
||||
key: key,
|
||||
color: _widgetSelectionButtonsBackgroundColor(context),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: onPressed,
|
||||
child: const Icon(
|
||||
CupertinoIcons.search,
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark,
|
||||
size: 28.0,
|
||||
color: CupertinoColors.white,
|
||||
color: _widgetSelectionButtonsForegroundColor(context),
|
||||
semanticLabel: 'Exit Select Widget mode.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _moveExitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
bool isLeftAligned = true,
|
||||
}) {
|
||||
return CupertinoButton(
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
child: Icon(
|
||||
isLeftAligned ? CupertinoIcons.arrow_right : CupertinoIcons.arrow_left,
|
||||
size: 32.0,
|
||||
color: _widgetSelectionButtonsBackgroundColor(context),
|
||||
semanticLabel:
|
||||
'Move "Exit Select Widget mode" button to the ${isLeftAligned ? 'right' : 'left'}.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _widgetSelectionButtonsForegroundColor(BuildContext context) {
|
||||
return CupertinoTheme.of(context).primaryContrastingColor;
|
||||
}
|
||||
|
||||
Color _widgetSelectionButtonsBackgroundColor(BuildContext context) {
|
||||
return CupertinoTheme.of(context).primaryColor;
|
||||
}
|
||||
|
||||
WidgetsApp _buildWidgetApp(BuildContext context) {
|
||||
final CupertinoThemeData effectiveThemeData = CupertinoTheme.of(context);
|
||||
final Color color = CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context);
|
||||
@ -572,7 +605,9 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
showPerformanceOverlay: widget.showPerformanceOverlay,
|
||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder:
|
||||
_moveExitWidgetSelectionButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
@ -606,7 +641,9 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
showPerformanceOverlay: widget.showPerformanceOverlay,
|
||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder:
|
||||
_moveExitWidgetSelectionButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
|
@ -22,6 +22,7 @@ import 'package:flutter/services.dart';
|
||||
import 'arc.dart';
|
||||
import 'colors.dart';
|
||||
import 'floating_action_button.dart';
|
||||
import 'icon_button.dart';
|
||||
import 'icons.dart';
|
||||
import 'material_localizations.dart';
|
||||
import 'page.dart';
|
||||
@ -904,6 +905,9 @@ class MaterialScrollBehavior extends ScrollBehavior {
|
||||
}
|
||||
|
||||
class _MaterialAppState extends State<MaterialApp> {
|
||||
static const double _moveExitWidgetSelectionIconSize = 32;
|
||||
static const double _moveExitWidgetSelectionTargetSize = 40;
|
||||
|
||||
late HeroController _heroController;
|
||||
|
||||
bool get _usesRouter => widget.routerDelegate != null || widget.routerConfig != null;
|
||||
@ -934,14 +938,65 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
];
|
||||
}
|
||||
|
||||
Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||
Widget _exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return FloatingActionButton(
|
||||
key: key,
|
||||
onPressed: onPressed,
|
||||
mini: true,
|
||||
child: const Icon(Icons.search),
|
||||
backgroundColor: _widgetSelectionButtonsBackgroundColor(context),
|
||||
foregroundColor: _widgetSelectionButtonsForegroundColor(context),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
semanticLabel: 'Exit Select Widget mode.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _moveExitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
bool isLeftAligned = true,
|
||||
}) {
|
||||
return IconButton(
|
||||
color: _widgetSelectionButtonsBackgroundColor(context),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: _moveExitWidgetSelectionIconSize,
|
||||
onPressed: onPressed,
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: _moveExitWidgetSelectionTargetSize,
|
||||
minHeight: _moveExitWidgetSelectionTargetSize,
|
||||
),
|
||||
icon: Icon(
|
||||
isLeftAligned ? Icons.arrow_right : Icons.arrow_left,
|
||||
semanticLabel:
|
||||
'Move "Exit Select Widget mode" button to the ${isLeftAligned ? 'right' : 'left'}.',
|
||||
));
|
||||
}
|
||||
|
||||
Color _widgetSelectionButtonsForegroundColor(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return _isDarkTheme(context)
|
||||
? theme.colorScheme.onPrimaryContainer
|
||||
: theme.colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
Color _widgetSelectionButtonsBackgroundColor(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return _isDarkTheme(context)
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.onPrimaryContainer;
|
||||
}
|
||||
|
||||
bool _isDarkTheme(BuildContext context) {
|
||||
return widget.themeMode == ThemeMode.dark ||
|
||||
widget.themeMode == ThemeMode.system &&
|
||||
MediaQuery.platformBrightnessOf(context) == Brightness.dark;
|
||||
}
|
||||
|
||||
ThemeData _themeBuilder(BuildContext context) {
|
||||
ThemeData? theme;
|
||||
// Resolve which theme to use based on brightness and high contrast.
|
||||
@ -1044,7 +1099,9 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
showPerformanceOverlay: widget.showPerformanceOverlay,
|
||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder:
|
||||
_moveExitWidgetSelectionButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
@ -1078,7 +1135,9 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
showPerformanceOverlay: widget.showPerformanceOverlay,
|
||||
showSemanticsDebugger: widget.showSemanticsDebugger,
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder:
|
||||
_moveExitWidgetSelectionButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
|
@ -341,7 +341,8 @@ class WidgetsApp extends StatefulWidget {
|
||||
this.showSemanticsDebugger = false,
|
||||
this.debugShowWidgetInspector = false,
|
||||
this.debugShowCheckedModeBanner = true,
|
||||
this.inspectorSelectButtonBuilder,
|
||||
this.exitWidgetSelectionButtonBuilder,
|
||||
this.moveExitWidgetSelectionButtonBuilder,
|
||||
this.shortcuts,
|
||||
this.actions,
|
||||
this.restorationScopeId,
|
||||
@ -439,7 +440,8 @@ class WidgetsApp extends StatefulWidget {
|
||||
this.showSemanticsDebugger = false,
|
||||
this.debugShowWidgetInspector = false,
|
||||
this.debugShowCheckedModeBanner = true,
|
||||
this.inspectorSelectButtonBuilder,
|
||||
this.exitWidgetSelectionButtonBuilder,
|
||||
this.moveExitWidgetSelectionButtonBuilder,
|
||||
this.shortcuts,
|
||||
this.actions,
|
||||
this.restorationScopeId,
|
||||
@ -1026,13 +1028,20 @@ class WidgetsApp extends StatefulWidget {
|
||||
/// debug mode.
|
||||
final bool debugShowWidgetInspector;
|
||||
|
||||
/// Builds the widget the [WidgetInspector] uses to switch between view and
|
||||
/// inspect modes.
|
||||
/// Builds the widget the [WidgetInspector] uses to exit selection mode.
|
||||
///
|
||||
/// This lets [MaterialApp] to use a Material Design button to toggle the
|
||||
/// This lets [MaterialApp] to use a Material Design button to exit the
|
||||
/// inspector select mode without requiring [WidgetInspector] to depend on the
|
||||
/// material package.
|
||||
final InspectorSelectButtonBuilder? inspectorSelectButtonBuilder;
|
||||
/// Material package.
|
||||
final ExitWidgetSelectionButtonBuilder? exitWidgetSelectionButtonBuilder;
|
||||
|
||||
/// Builds the widget the [WidgetInspector] uses to move the exit selection
|
||||
/// mode button.
|
||||
///
|
||||
/// This lets [MaterialApp] to use a Material Design button to change the
|
||||
/// alignment without requiring [WidgetInspector] to depend on the Material
|
||||
/// package.
|
||||
final MoveExitWidgetSelectionButtonBuilder? moveExitWidgetSelectionButtonBuilder;
|
||||
|
||||
/// {@template flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
|
||||
/// Turns on a little "DEBUG" banner in debug mode to indicate
|
||||
@ -1755,7 +1764,8 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
builder: (BuildContext context, bool debugShowWidgetInspectorOverride, Widget? child) {
|
||||
if (widget.debugShowWidgetInspector || debugShowWidgetInspectorOverride) {
|
||||
return WidgetInspector(
|
||||
selectButtonBuilder: widget.inspectorSelectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: widget.exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: widget.moveExitWidgetSelectionButtonBuilder,
|
||||
child: child!,
|
||||
);
|
||||
}
|
||||
|
@ -37,8 +37,20 @@ import 'service_extensions.dart';
|
||||
import 'view.dart';
|
||||
|
||||
/// Signature for the builder callback used by
|
||||
/// [WidgetInspector.selectButtonBuilder].
|
||||
typedef InspectorSelectButtonBuilder = Widget Function(BuildContext context, VoidCallback onPressed);
|
||||
/// [WidgetInspector.exitWidgetSelectionButtonBuilder].
|
||||
typedef ExitWidgetSelectionButtonBuilder = Widget Function(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
});
|
||||
|
||||
/// Signature for the builder callback used by
|
||||
/// [WidgetInspector.moveExitWidgetSelectionButtonBuilder].
|
||||
typedef MoveExitWidgetSelectionButtonBuilder = Widget Function(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
bool isLeftAligned,
|
||||
});
|
||||
|
||||
/// Signature for a method that registers the service extension `callback` with
|
||||
/// the given `name`.
|
||||
@ -774,13 +786,15 @@ mixin WidgetInspectorService {
|
||||
static WidgetInspectorService get instance => _instance;
|
||||
static WidgetInspectorService _instance = _WidgetInspectorService();
|
||||
|
||||
/// Whether the inspector is in select mode.
|
||||
/// Enables select mode for the Inspector.
|
||||
///
|
||||
/// 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.
|
||||
@visibleForTesting
|
||||
final ValueNotifier<bool> isSelectMode = ValueNotifier<bool>(true);
|
||||
set isSelectMode(bool enabled) {
|
||||
_changeWidgetSelectionMode(enabled);
|
||||
}
|
||||
|
||||
@protected
|
||||
static set instance(WidgetInspectorService instance) {
|
||||
@ -1622,6 +1636,13 @@ mixin WidgetInspectorService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes whether widget selection mode is [enabled].
|
||||
void _changeWidgetSelectionMode(bool enabled) {
|
||||
WidgetsBinding.instance.debugShowWidgetInspectorOverride = enabled;
|
||||
_postExtensionStateChangedEvent(
|
||||
WidgetInspectorServiceExtensions.show.name, enabled);
|
||||
}
|
||||
|
||||
/// Returns a DevTools uri linking to a specific element on the inspector page.
|
||||
String? _devToolsInspectorUriForElement(Element element) {
|
||||
if (activeDevToolsServerAddress != null && connectedVmServiceUri != null) {
|
||||
@ -2758,17 +2779,28 @@ class WidgetInspector extends StatefulWidget {
|
||||
const WidgetInspector({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.selectButtonBuilder,
|
||||
required this.exitWidgetSelectionButtonBuilder,
|
||||
required this.moveExitWidgetSelectionButtonBuilder,
|
||||
});
|
||||
|
||||
/// The widget that is being inspected.
|
||||
final Widget child;
|
||||
|
||||
/// A builder that is called to create the select button.
|
||||
/// A builder that is called to create the exit select-mode button.
|
||||
///
|
||||
/// The `onPressed` callback and key passed as arguments to the builder should
|
||||
/// be hooked up to the returned widget.
|
||||
final ExitWidgetSelectionButtonBuilder? exitWidgetSelectionButtonBuilder;
|
||||
|
||||
/// A builder that is called to create the button that moves the exit select-
|
||||
/// mode button to the right or left.
|
||||
///
|
||||
/// The `onPressed` callback passed as an argument to the builder should be
|
||||
/// hooked up to the returned widget.
|
||||
final InspectorSelectButtonBuilder? selectButtonBuilder;
|
||||
///
|
||||
/// The button UI should respond to the `leftAligned` argument.
|
||||
final MoveExitWidgetSelectionButtonBuilder?
|
||||
moveExitWidgetSelectionButtonBuilder;
|
||||
|
||||
@override
|
||||
State<WidgetInspector> createState() => _WidgetInspectorState();
|
||||
@ -2797,24 +2829,24 @@ class _WidgetInspectorState extends State<WidgetInspector>
|
||||
|
||||
WidgetInspectorService.instance.selection
|
||||
.addListener(_selectionInformationChanged);
|
||||
WidgetInspectorService.instance.isSelectMode
|
||||
WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier
|
||||
.addListener(_selectionInformationChanged);
|
||||
selection = WidgetInspectorService.instance.selection;
|
||||
isSelectMode = WidgetInspectorService.instance.isSelectMode.value;
|
||||
isSelectMode = WidgetsBinding.instance.debugShowWidgetInspectorOverride;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetInspectorService.instance.selection
|
||||
.removeListener(_selectionInformationChanged);
|
||||
WidgetInspectorService.instance.isSelectMode
|
||||
WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier
|
||||
.removeListener(_selectionInformationChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _selectionInformationChanged() => setState((){
|
||||
selection = WidgetInspectorService.instance.selection;
|
||||
isSelectMode = WidgetInspectorService.instance.isSelectMode.value;
|
||||
isSelectMode = WidgetsBinding.instance.debugShowWidgetInspectorOverride;
|
||||
});
|
||||
|
||||
bool _hitTestHelper(
|
||||
@ -2937,22 +2969,13 @@ class _WidgetInspectorState extends State<WidgetInspector>
|
||||
_inspectAt(_lastPointerLocation!);
|
||||
WidgetInspectorService.instance._sendInspectEvent(selection.current);
|
||||
}
|
||||
|
||||
// Only exit select mode if there is a button to return to select mode.
|
||||
if (widget.selectButtonBuilder != null) {
|
||||
WidgetInspectorService.instance.isSelectMode.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleEnableSelect() {
|
||||
WidgetInspectorService.instance.isSelectMode.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Be careful changing this build method. The _InspectorOverlayLayer
|
||||
// assumes the root RenderObject for the WidgetInspector will be
|
||||
// a RenderStack with a _RenderInspectorOverlay as the last child.
|
||||
// a RenderStack containing a _RenderInspectorOverlay as a child.
|
||||
return Stack(children: <Widget>[
|
||||
GestureDetector(
|
||||
onTap: _handleTap,
|
||||
@ -2967,13 +2990,14 @@ class _WidgetInspectorState extends State<WidgetInspector>
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
if (!isSelectMode && widget.selectButtonBuilder != null)
|
||||
Positioned(
|
||||
left: _kInspectButtonMargin,
|
||||
bottom: _kInspectButtonMargin,
|
||||
child: widget.selectButtonBuilder!(context, _handleEnableSelect),
|
||||
),
|
||||
_InspectorOverlay(selection: selection),
|
||||
if (isSelectMode && widget.exitWidgetSelectionButtonBuilder != null)
|
||||
_ExitWidgetSelectionButtonGroup(
|
||||
exitWidgetSelectionButtonBuilder:
|
||||
widget.exitWidgetSelectionButtonBuilder!,
|
||||
moveExitWidgetSelectionButtonBuilder:
|
||||
widget.moveExitWidgetSelectionButtonBuilder,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -3429,7 +3453,10 @@ class _InspectorOverlayLayer extends Layer {
|
||||
while (current != null) {
|
||||
// We found the widget inspector render object.
|
||||
if (current is RenderStack
|
||||
&& current.lastChild is _RenderInspectorOverlay) {
|
||||
&&
|
||||
current.getChildrenAsList().any(
|
||||
(RenderBox child) => child is _RenderInspectorOverlay,
|
||||
)) {
|
||||
return rootRenderObject == current;
|
||||
}
|
||||
current = current.parent;
|
||||
@ -3440,7 +3467,6 @@ class _InspectorOverlayLayer extends Layer {
|
||||
|
||||
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.
|
||||
@ -3452,6 +3478,304 @@ const TextStyle _messageStyle = TextStyle(
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
class _ExitWidgetSelectionButtonGroup extends StatefulWidget {
|
||||
const _ExitWidgetSelectionButtonGroup({
|
||||
required this.exitWidgetSelectionButtonBuilder,
|
||||
required this.moveExitWidgetSelectionButtonBuilder,
|
||||
});
|
||||
|
||||
final ExitWidgetSelectionButtonBuilder exitWidgetSelectionButtonBuilder;
|
||||
final MoveExitWidgetSelectionButtonBuilder?
|
||||
moveExitWidgetSelectionButtonBuilder;
|
||||
|
||||
@override
|
||||
State<_ExitWidgetSelectionButtonGroup> createState() =>
|
||||
_ExitWidgetSelectionButtonGroupState();
|
||||
}
|
||||
|
||||
class _ExitWidgetSelectionButtonGroupState
|
||||
extends State<_ExitWidgetSelectionButtonGroup> {
|
||||
static const double _kExitWidgetSelectionButtonPadding = 4.0;
|
||||
static const double _kExitWidgetSelectionButtonMargin = 10.0;
|
||||
|
||||
final GlobalKey _exitWidgetSelectionButtonKey = GlobalKey(
|
||||
debugLabel: 'Exit Widget Selection button',
|
||||
);
|
||||
|
||||
String? _tooltipMessage;
|
||||
|
||||
bool _leftAligned = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget? moveExitWidgetSelectionButton =
|
||||
widget.moveExitWidgetSelectionButtonBuilder != null
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: _leftAligned ? _kExitWidgetSelectionButtonPadding : 0.0,
|
||||
right:
|
||||
_leftAligned ? 0.0 : _kExitWidgetSelectionButtonPadding,
|
||||
),
|
||||
child: _TooltipGestureDetector(
|
||||
button: widget.moveExitWidgetSelectionButtonBuilder!(
|
||||
context,
|
||||
onPressed: () {
|
||||
_changeButtonGroupAlignment();
|
||||
_onTooltipHidden();
|
||||
},
|
||||
isLeftAligned: _leftAligned,
|
||||
),
|
||||
onTooltipVisible: () {
|
||||
_changeTooltipMessage(
|
||||
'Move to the ${_leftAligned ? 'right' : 'left'}',
|
||||
);
|
||||
},
|
||||
onTooltipHidden: _onTooltipHidden,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
final Widget buttonGroup = Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
children: <Widget>[
|
||||
CustomPaint(
|
||||
painter: _ExitWidgetSelectionTooltipPainter(
|
||||
tooltipMessage: _tooltipMessage,
|
||||
buttonKey: _exitWidgetSelectionButtonKey,
|
||||
isLeftAligned: _leftAligned,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
if (!_leftAligned && moveExitWidgetSelectionButton != null)
|
||||
moveExitWidgetSelectionButton,
|
||||
_TooltipGestureDetector(
|
||||
button: widget.exitWidgetSelectionButtonBuilder(
|
||||
context,
|
||||
onPressed: _exitWidgetSelectionMode,
|
||||
key: _exitWidgetSelectionButtonKey,
|
||||
),
|
||||
onTooltipVisible: () {
|
||||
_changeTooltipMessage('Exit Select Widget mode');
|
||||
},
|
||||
onTooltipHidden: _onTooltipHidden,
|
||||
),
|
||||
if (_leftAligned && moveExitWidgetSelectionButton != null)
|
||||
moveExitWidgetSelectionButton,
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Positioned(
|
||||
left: _leftAligned ? _kExitWidgetSelectionButtonMargin : null,
|
||||
right: _leftAligned ? null : _kExitWidgetSelectionButtonMargin,
|
||||
bottom: _kExitWidgetSelectionButtonMargin,
|
||||
child: buttonGroup,
|
||||
);
|
||||
}
|
||||
|
||||
void _exitWidgetSelectionMode() {
|
||||
WidgetInspectorService.instance._changeWidgetSelectionMode(false);
|
||||
}
|
||||
|
||||
void _changeButtonGroupAlignment() {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_leftAligned = !_leftAligned;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onTooltipHidden() {
|
||||
_changeTooltipMessage(null);
|
||||
}
|
||||
|
||||
void _changeTooltipMessage(String? message) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tooltipMessage = message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TooltipGestureDetector extends StatefulWidget {
|
||||
const _TooltipGestureDetector({
|
||||
required this.button,
|
||||
required this.onTooltipVisible,
|
||||
required this.onTooltipHidden,
|
||||
});
|
||||
|
||||
final Widget button;
|
||||
final void Function() onTooltipVisible;
|
||||
final void Function() onTooltipHidden;
|
||||
|
||||
static const Duration _tooltipShownOnLongPressDuration = Duration(
|
||||
milliseconds: 1500,
|
||||
);
|
||||
static const Duration _tooltipDelayDuration = Duration(
|
||||
milliseconds: 100,
|
||||
);
|
||||
|
||||
@override
|
||||
State<_TooltipGestureDetector> createState() =>
|
||||
_TooltipGestureDetectorState();
|
||||
}
|
||||
|
||||
class _TooltipGestureDetectorState extends State<_TooltipGestureDetector> {
|
||||
Timer? _tooltipVisibleTimer;
|
||||
Timer? _tooltipHiddenTimer;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tooltipVisibleTimer?.cancel();
|
||||
_tooltipVisibleTimer = null;
|
||||
_tooltipHiddenTimer?.cancel();
|
||||
_tooltipHiddenTimer = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onLongPress: () {
|
||||
_tooltipVisibleAfter(_TooltipGestureDetector._tooltipDelayDuration);
|
||||
_tooltipHiddenAfter(
|
||||
_TooltipGestureDetector._tooltipShownOnLongPressDuration +
|
||||
_TooltipGestureDetector._tooltipDelayDuration,
|
||||
);
|
||||
},
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
_tooltipVisibleAfter(
|
||||
_TooltipGestureDetector._tooltipDelayDuration);
|
||||
},
|
||||
onExit: (_) {
|
||||
_tooltipHiddenAfter(
|
||||
_TooltipGestureDetector._tooltipDelayDuration);
|
||||
},
|
||||
child: widget.button,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _tooltipVisibleAfter(Duration duration) {
|
||||
_tooltipVisibilityChangedAfter(duration, isVisible: true);
|
||||
}
|
||||
|
||||
void _tooltipHiddenAfter(Duration duration) {
|
||||
_tooltipVisibilityChangedAfter(duration, isVisible: false);
|
||||
}
|
||||
|
||||
void _tooltipVisibilityChangedAfter(
|
||||
Duration duration, {
|
||||
required bool isVisible,
|
||||
}) {
|
||||
final Timer? timer = isVisible ? _tooltipVisibleTimer : _tooltipHiddenTimer;
|
||||
if (timer?.isActive ?? false) {
|
||||
timer!.cancel();
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
_tooltipVisibleTimer = Timer(duration, () {
|
||||
widget.onTooltipVisible();
|
||||
});
|
||||
} else {
|
||||
_tooltipHiddenTimer = Timer(duration, () {
|
||||
widget.onTooltipHidden();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ExitWidgetSelectionTooltipPainter extends CustomPainter {
|
||||
_ExitWidgetSelectionTooltipPainter({
|
||||
required this.tooltipMessage,
|
||||
required this.buttonKey,
|
||||
required this.isLeftAligned,
|
||||
});
|
||||
|
||||
final String? tooltipMessage;
|
||||
final GlobalKey buttonKey;
|
||||
final bool isLeftAligned;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// Do not paint the tooltip if it is currently hidden.
|
||||
final bool isVisible = tooltipMessage != null;
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not paint the tooltip if the exit select mode button is not rendered.
|
||||
final RenderObject? buttonRenderObject =
|
||||
buttonKey.currentContext?.findRenderObject();
|
||||
if (buttonRenderObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Define tooltip appearance.
|
||||
const double tooltipPadding = 4.0;
|
||||
const double tooltipSpacing = 6.0;
|
||||
|
||||
final TextPainter tooltipTextPainter = TextPainter()
|
||||
..maxLines = 1
|
||||
..ellipsis = '...'
|
||||
..text = TextSpan(
|
||||
text: tooltipMessage,
|
||||
style: _messageStyle,
|
||||
)
|
||||
..textDirection = TextDirection.ltr
|
||||
..layout();
|
||||
|
||||
final Paint tooltipPaint = Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = _kTooltipBackgroundColor;
|
||||
|
||||
// Determine tooltip position.
|
||||
final double buttonWidth = buttonRenderObject.paintBounds.width;
|
||||
final Size textSize = tooltipTextPainter.size;
|
||||
final double textWidth = textSize.width;
|
||||
final double textHeight = textSize.height;
|
||||
final double tooltipWidth = textWidth + (tooltipPadding * 2);
|
||||
final double tooltipHeight = textHeight + (tooltipPadding * 2);
|
||||
|
||||
final double tooltipXOffset =
|
||||
isLeftAligned ? 0 - buttonWidth : 0 - (tooltipWidth - buttonWidth);
|
||||
final double tooltipYOffset = 0 - tooltipHeight - tooltipSpacing;
|
||||
|
||||
// Draw tooltip background.
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(
|
||||
tooltipXOffset,
|
||||
tooltipYOffset,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
),
|
||||
tooltipPaint,
|
||||
);
|
||||
|
||||
// Draw tooltip text.
|
||||
tooltipTextPainter.paint(
|
||||
canvas,
|
||||
Offset(
|
||||
tooltipXOffset + tooltipPadding,
|
||||
tooltipYOffset + tooltipPadding,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant _ExitWidgetSelectionTooltipPainter oldDelegate) {
|
||||
return tooltipMessage != oldDelegate.tooltipMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// Interface for classes that track the source code location the their
|
||||
/// constructor was called from.
|
||||
///
|
||||
|
@ -285,9 +285,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
static void runTests() {
|
||||
final TestWidgetInspectorService service = TestWidgetInspectorService();
|
||||
WidgetInspectorService.instance = service;
|
||||
setUp(() {
|
||||
WidgetInspectorService.instance.isSelectMode.value = true;
|
||||
});
|
||||
tearDown(() async {
|
||||
service.resetAllState();
|
||||
|
||||
@ -357,7 +354,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
selectButtonBuilder: null,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Text('a', textDirection: TextDirection.ltr),
|
||||
@ -373,13 +371,23 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets('WidgetInspector interaction test', (WidgetTester tester) async {
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
final List<String> log = <String>[];
|
||||
final GlobalKey selectButtonKey = GlobalKey();
|
||||
late GlobalKey exitWidgetSelectionButtonKey;
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
final GlobalKey topButtonKey = GlobalKey();
|
||||
final GlobalKey bottomButtonKey = GlobalKey();
|
||||
|
||||
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
exitWidgetSelectionButtonKey = key;
|
||||
return Material(
|
||||
child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
}
|
||||
|
||||
String paragraphText(RenderParagraph paragraph) {
|
||||
@ -387,12 +395,32 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
return textSpan.text!;
|
||||
}
|
||||
|
||||
|
||||
Future<void> tapAndVerifyWidgetSelection(
|
||||
Finder widgetFinder, {
|
||||
required bool isSelected,
|
||||
required GlobalKey widgetKey,
|
||||
}) async {
|
||||
// Tap on the widget.
|
||||
await tester.tap(widgetFinder, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the tap was intercepted by the Widget Inspector.
|
||||
final RenderObject renderObject =
|
||||
find.byKey(widgetKey).evaluate().first.renderObject!;
|
||||
expect(
|
||||
WidgetInspectorService.instance.selection.candidates,
|
||||
contains(renderObject),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
selectButtonBuilder: selectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: Material(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
@ -404,6 +432,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: const Text('TOP'),
|
||||
),
|
||||
ElevatedButton(
|
||||
key: bottomButtonKey,
|
||||
onPressed: () {
|
||||
log.add('bottom');
|
||||
},
|
||||
@ -417,9 +446,13 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
);
|
||||
|
||||
expect(WidgetInspectorService.instance.selection.current, isNull);
|
||||
await tester.tap(find.text('TOP'), warnIfMissed: false);
|
||||
await tester.pump();
|
||||
// Tap intercepted by the inspector
|
||||
|
||||
// Tap on the top button and verify it's selected in the Inspector.
|
||||
await tapAndVerifyWidgetSelection(
|
||||
find.text('TOP'),
|
||||
isSelected: true,
|
||||
widgetKey: topButtonKey,
|
||||
);
|
||||
expect(log, equals(<String>[]));
|
||||
expect(
|
||||
paragraphText(
|
||||
@ -427,35 +460,30 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
),
|
||||
equals('TOP'),
|
||||
);
|
||||
final RenderObject topButton = find.byKey(topButtonKey).evaluate().first.renderObject!;
|
||||
expect(
|
||||
WidgetInspectorService.instance.selection.candidates,
|
||||
contains(topButton),
|
||||
);
|
||||
|
||||
// Tap on the bottom button and verify it's selected in the Inspector.
|
||||
await tapAndVerifyWidgetSelection(
|
||||
find.text('BOTTOM'),
|
||||
isSelected: true,
|
||||
widgetKey: bottomButtonKey,
|
||||
);
|
||||
expect(
|
||||
paragraphText(
|
||||
WidgetInspectorService.instance.selection.current! as RenderParagraph,
|
||||
),
|
||||
equals('BOTTOM'),
|
||||
);
|
||||
expect(log, equals(<String>[]));
|
||||
|
||||
// Now exit selection mode by tapping the Exit Selection Mode button.
|
||||
await tester.tap(find.byKey(exitWidgetSelectionButtonKey));
|
||||
await tester.pump();
|
||||
|
||||
// Tap on the top button and verify it is not selected in the Inspector.
|
||||
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(
|
||||
WidgetInspectorService.instance.selection.current! as RenderParagraph,
|
||||
),
|
||||
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'), warnIfMissed: false);
|
||||
expect(log, equals(<String>[]));
|
||||
log.clear();
|
||||
// Ensure the inspector selection is still BOTTOM (not TOP).
|
||||
expect(
|
||||
paragraphText(
|
||||
WidgetInspectorService.instance.selection.current! as RenderParagraph,
|
||||
@ -469,7 +497,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
selectButtonBuilder: null,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(0.0),
|
||||
child: const Stack(
|
||||
@ -490,12 +519,21 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets('WidgetInspector scroll test', (WidgetTester tester) async {
|
||||
final Key childKey = UniqueKey();
|
||||
final GlobalKey selectButtonKey = GlobalKey();
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
|
||||
final Key childKey = UniqueKey();
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
late GlobalKey exitWidgetSelectionButtonKey;
|
||||
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
exitWidgetSelectionButtonKey = key;
|
||||
return Material(
|
||||
child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
@ -503,7 +541,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
selectButtonBuilder: selectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: ListView(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
children: <Widget>[
|
||||
@ -534,7 +573,11 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
await tester.pump();
|
||||
expect(WidgetInspectorService.instance.selection.current, isNotNull);
|
||||
|
||||
// Now out of inspect mode due to the click.
|
||||
// Now exit selection mode by tapping the Exit Selection Mode button.
|
||||
await tester.tap(find.byKey(exitWidgetSelectionButtonKey));
|
||||
await tester.pump();
|
||||
|
||||
// Now out of inspect mode due to clicking the Exit Selection Mode button.
|
||||
await tester.fling(find.byType(ListView), const Offset(0.0, -200.0), 200.0);
|
||||
await tester.pump();
|
||||
|
||||
@ -547,13 +590,17 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets('WidgetInspector long press', (WidgetTester tester) async {
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
bool didLongPress = false;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
selectButtonBuilder: null,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
expect(didLongPress, isFalse);
|
||||
@ -571,6 +618,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets('WidgetInspector offstage', (WidgetTester tester) async {
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
final GlobalKey clickTarget = GlobalKey();
|
||||
|
||||
@ -601,7 +651,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
selectButtonBuilder: null,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
entry1 = OverlayEntry(
|
||||
@ -642,6 +693,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets('WidgetInspector with Transform above', (WidgetTester tester) async {
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
final GlobalKey childKey = GlobalKey();
|
||||
final GlobalKey repaintBoundaryKey = GlobalKey();
|
||||
|
||||
@ -660,7 +714,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
selectButtonBuilder: null,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: ColoredBox(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
@ -689,6 +744,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets('Multiple widget inspectors', (WidgetTester tester) async {
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
// This test verifies that interacting with different inspectors
|
||||
// works correctly. This use case may be an app that displays multiple
|
||||
// apps inside (i.e. a storyboard).
|
||||
@ -701,8 +759,13 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
final GlobalKey child1Key = GlobalKey();
|
||||
final GlobalKey child2Key = GlobalKey();
|
||||
|
||||
InspectorSelectButtonBuilder selectButtonBuilder(Key key) {
|
||||
return (BuildContext context, VoidCallback onPressed) {
|
||||
ExitWidgetSelectionButtonBuilder exitWidgetSelectionButtonBuilder(
|
||||
Key key) {
|
||||
return (
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
};
|
||||
}
|
||||
@ -720,7 +783,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Flexible(
|
||||
child: WidgetInspector(
|
||||
key: inspector1Key,
|
||||
selectButtonBuilder: selectButtonBuilder(selectButton1Key),
|
||||
exitWidgetSelectionButtonBuilder:
|
||||
exitWidgetSelectionButtonBuilder(selectButton1Key),
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: Container(
|
||||
key: child1Key,
|
||||
child: const Text('Child 1'),
|
||||
@ -730,7 +795,9 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Flexible(
|
||||
child: WidgetInspector(
|
||||
key: inspector2Key,
|
||||
selectButtonBuilder: selectButtonBuilder(selectButton2Key),
|
||||
exitWidgetSelectionButtonBuilder:
|
||||
exitWidgetSelectionButtonBuilder(selectButton2Key),
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: Container(
|
||||
key: child2Key,
|
||||
child: const Text('Child 2'),
|
||||
@ -751,10 +818,6 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
equals('Child 1'),
|
||||
);
|
||||
|
||||
// Re-enable select mode since it's state is shared between the
|
||||
// WidgetInspectors
|
||||
WidgetInspectorService.instance.isSelectMode.value = true;
|
||||
|
||||
await tester.tap(find.text('Child 2'), warnIfMissed: false);
|
||||
await tester.pump();
|
||||
expect(
|
||||
@ -766,14 +829,20 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'WidgetInspector selectButton inspection for tap',
|
||||
'WidgetInspector Exit Selection Mode button',
|
||||
(WidgetTester tester) async {
|
||||
final GlobalKey selectButtonKey = GlobalKey();
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
setupDefaultPubRootDirectory(service);
|
||||
|
||||
Widget selectButtonBuilder(BuildContext context, VoidCallback onPressed) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: selectButtonKey, child: null));
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
@ -781,7 +850,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
selectButtonBuilder: selectButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: const Text('Child 1'),
|
||||
),
|
||||
),
|
||||
@ -815,6 +885,94 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
skip: !WidgetInspectorService.instance.isWidgetCreationTracked() // [intended] Test requires --track-widget-creation flag.
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'WidgetInspector Move Exit Selection Mode button to the right / left',
|
||||
(WidgetTester tester) async {
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
setupDefaultPubRootDirectory(service);
|
||||
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
key: key,
|
||||
child: const Text('EXIT SELECT MODE'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget moveWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
bool isLeftAligned = true,
|
||||
}) {
|
||||
return Material(
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(isLeftAligned ? 'MOVE RIGHT' : 'MOVE LEFT'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Finder buttonFinder(String buttonText) {
|
||||
return find.ancestor(
|
||||
of: find.text(buttonText),
|
||||
matching: find.byType(ElevatedButton),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder:
|
||||
moveWidgetSelectionButtonBuilder,
|
||||
child: const Text('APP'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Intitially the exit select button is on the left.
|
||||
final Finder exitButton = buttonFinder('EXIT SELECT MODE');
|
||||
expect(exitButton, findsOneWidget);
|
||||
final Finder moveRightButton = buttonFinder('MOVE RIGHT');
|
||||
expect(moveRightButton, findsOneWidget);
|
||||
final double initialExitButtonX = tester.getCenter(exitButton).dx;
|
||||
|
||||
// Move the button to the right.
|
||||
await tester.tap(moveRightButton);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the button is now on the right.
|
||||
expect(moveRightButton, findsNothing);
|
||||
final Finder moveLeftButton = buttonFinder('MOVE LEFT');
|
||||
expect(moveLeftButton, findsOneWidget);
|
||||
final double exitButtonXAfterMovingRight =
|
||||
tester.getCenter(exitButton).dx;
|
||||
expect(initialExitButtonX, lessThan(exitButtonXAfterMovingRight));
|
||||
|
||||
// Move the button to the left again.
|
||||
await tester.tap(moveLeftButton);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the button is in its original position.
|
||||
expect(moveLeftButton, findsNothing);
|
||||
expect(moveRightButton, findsOneWidget);
|
||||
final double exitButtonXAfterMovingLeft = tester.getCenter(exitButton).dx;
|
||||
expect(exitButtonXAfterMovingLeft, equals(initialExitButtonX));
|
||||
},
|
||||
skip: !WidgetInspectorService.instance.isWidgetCreationTracked() // [intended] Test requires --track-widget-creation flag.
|
||||
);
|
||||
|
||||
testWidgets('test transformDebugCreator will re-order if after stack trace', (WidgetTester tester) async {
|
||||
final bool widgetTracked = WidgetInspectorService.instance.isWidgetCreationTracked();
|
||||
await tester.pumpWidget(
|
||||
@ -3727,7 +3885,8 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
final Widget widget = Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
selectButtonBuilder: null,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: _applyConstructor(_TrivialWidget.new),
|
||||
),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user