[beta] CP request for https://github.com/flutter/flutter/pull/167677 (#168386)
### CP request for https://github.com/flutter/flutter/pull/167677 into flutter-3.32-candidate.0 **Impacted Users:** Some subset of widget inspector users with a specific configuration of `package:go_router`, see upvotes and comments on https://github.com/flutter/flutter/issues/166118. **Impact Description:** Depending on how users have configured their routes using `package:go_router`, enabling / disabling the widget inspector can be destructive to their app's routing state, preventing them from inspecting widgets on secondary screens of their app. **Workaround:** No workaround available. **Risk:** Low **Test Coverage:** Yes, tests were added and this has been manually tested as well **Validation Steps:** - Run a Flutter app - Open the DevTools Inspector for the running app - Toggle "Select widget mode" - An additional button has been added to the on-device inspector that allows developers to both interact with their app (e.g. navigate to a new page) and select widgets while in Widget Selection mode. See gif below. 
This commit is contained in:
parent
87cd28951e
commit
8545480266
@ -15,6 +15,7 @@ import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'button.dart';
|
||||
import 'colors.dart';
|
||||
import 'constants.dart';
|
||||
import 'icons.dart';
|
||||
import 'interface_level.dart';
|
||||
import 'localizations.dart';
|
||||
@ -534,46 +535,44 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
Widget _exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return CupertinoButton(
|
||||
key: key,
|
||||
color: _widgetSelectionButtonsBackgroundColor(context),
|
||||
padding: EdgeInsets.zero,
|
||||
return _CupertinoInspectorButton.filled(
|
||||
onPressed: onPressed,
|
||||
child: Icon(
|
||||
CupertinoIcons.xmark,
|
||||
size: 28.0,
|
||||
color: _widgetSelectionButtonsForegroundColor(context),
|
||||
semanticLabel: 'Exit Select Widget mode.',
|
||||
),
|
||||
semanticLabel: semanticLabel,
|
||||
icon: CupertinoIcons.xmark,
|
||||
buttonKey: key,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _moveExitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
bool isLeftAligned = true,
|
||||
}) {
|
||||
return CupertinoButton(
|
||||
return _CupertinoInspectorButton.iconOnly(
|
||||
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'}.',
|
||||
),
|
||||
semanticLabel: semanticLabel,
|
||||
icon: isLeftAligned ? CupertinoIcons.arrow_right : CupertinoIcons.arrow_left,
|
||||
);
|
||||
}
|
||||
|
||||
Color _widgetSelectionButtonsForegroundColor(BuildContext context) {
|
||||
return CupertinoTheme.of(context).primaryContrastingColor;
|
||||
}
|
||||
|
||||
Color _widgetSelectionButtonsBackgroundColor(BuildContext context) {
|
||||
return CupertinoTheme.of(context).primaryColor;
|
||||
Widget _tapBehaviorButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required bool selectionOnTapEnabled,
|
||||
}) {
|
||||
return _CupertinoInspectorButton.toggle(
|
||||
onPressed: onPressed,
|
||||
semanticLabel: semanticLabel,
|
||||
// This icon is also used for the Material-styled button and for DevTools.
|
||||
// It should be updated in all 3 places if changed.
|
||||
icon: CupertinoIcons.cursor_rays,
|
||||
toggledOn: selectionOnTapEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
WidgetsApp _buildWidgetApp(BuildContext context) {
|
||||
@ -607,6 +606,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: _moveExitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: _tapBehaviorButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
@ -642,6 +642,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: _moveExitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: _tapBehaviorButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
@ -680,3 +681,83 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CupertinoInspectorButton extends InspectorButton {
|
||||
const _CupertinoInspectorButton.filled({
|
||||
required super.onPressed,
|
||||
required super.semanticLabel,
|
||||
required super.icon,
|
||||
super.buttonKey,
|
||||
}) : super.filled();
|
||||
|
||||
const _CupertinoInspectorButton.toggle({
|
||||
required super.onPressed,
|
||||
required super.semanticLabel,
|
||||
required super.icon,
|
||||
super.toggledOn,
|
||||
}) : super.toggle();
|
||||
|
||||
const _CupertinoInspectorButton.iconOnly({
|
||||
required super.onPressed,
|
||||
required super.semanticLabel,
|
||||
required super.icon,
|
||||
}) : super.iconOnly();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Icon buttonIcon = Icon(
|
||||
icon,
|
||||
semanticLabel: semanticLabel,
|
||||
size: iconSizeForVariant,
|
||||
color: foregroundColor(context),
|
||||
);
|
||||
|
||||
return Padding(
|
||||
key: buttonKey,
|
||||
padding: const EdgeInsets.all(
|
||||
(kMinInteractiveDimensionCupertino - InspectorButton.buttonSize) / 2,
|
||||
),
|
||||
child:
|
||||
variant == InspectorButtonVariant.toggle && !toggledOn!
|
||||
? CupertinoButton.tinted(
|
||||
minSize: InspectorButton.buttonSize,
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
child: buttonIcon,
|
||||
)
|
||||
: CupertinoButton(
|
||||
minSize: InspectorButton.buttonSize,
|
||||
onPressed: onPressed,
|
||||
padding: EdgeInsets.zero,
|
||||
color: backgroundColor(context),
|
||||
child: buttonIcon,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Color foregroundColor(BuildContext context) {
|
||||
final Color primaryColor = CupertinoTheme.of(context).primaryColor;
|
||||
final Color secondaryColor = CupertinoTheme.of(context).primaryContrastingColor;
|
||||
switch (variant) {
|
||||
case InspectorButtonVariant.filled:
|
||||
return secondaryColor;
|
||||
case InspectorButtonVariant.iconOnly:
|
||||
return primaryColor;
|
||||
case InspectorButtonVariant.toggle:
|
||||
return !toggledOn! ? primaryColor : secondaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Color backgroundColor(BuildContext context) {
|
||||
final Color primaryColor = CupertinoTheme.of(context).primaryColor;
|
||||
switch (variant) {
|
||||
case InspectorButtonVariant.filled:
|
||||
case InspectorButtonVariant.toggle:
|
||||
return primaryColor;
|
||||
case InspectorButtonVariant.iconOnly:
|
||||
return const Color(0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,8 +20,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'arc.dart';
|
||||
import 'button_style.dart';
|
||||
import 'colors.dart';
|
||||
import 'floating_action_button.dart';
|
||||
import 'icon_button.dart';
|
||||
import 'icons.dart';
|
||||
import 'material_localizations.dart';
|
||||
@ -29,6 +29,7 @@ import 'page.dart';
|
||||
import 'scaffold.dart' show ScaffoldMessenger, ScaffoldMessengerState;
|
||||
import 'scrollbar.dart';
|
||||
import 'theme.dart';
|
||||
import 'theme_data.dart';
|
||||
import 'tooltip.dart';
|
||||
|
||||
// Examples can assume:
|
||||
@ -903,9 +904,6 @@ 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;
|
||||
@ -938,52 +936,47 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
Widget _exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return FloatingActionButton(
|
||||
key: key,
|
||||
return _MaterialInspectorButton.filled(
|
||||
onPressed: onPressed,
|
||||
mini: true,
|
||||
backgroundColor: _widgetSelectionButtonsBackgroundColor(context),
|
||||
foregroundColor: _widgetSelectionButtonsForegroundColor(context),
|
||||
child: const Icon(Icons.close, semanticLabel: 'Exit Select Widget mode.'),
|
||||
semanticLabel: semanticLabel,
|
||||
icon: Icons.close,
|
||||
isDarkTheme: _isDarkTheme(context),
|
||||
buttonKey: key,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _moveExitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
bool isLeftAligned = true,
|
||||
}) {
|
||||
return IconButton(
|
||||
color: _widgetSelectionButtonsBackgroundColor(context),
|
||||
padding: EdgeInsets.zero,
|
||||
iconSize: _moveExitWidgetSelectionIconSize,
|
||||
return _MaterialInspectorButton.iconOnly(
|
||||
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'}.',
|
||||
),
|
||||
semanticLabel: semanticLabel,
|
||||
icon: isLeftAligned ? Icons.arrow_right : Icons.arrow_left,
|
||||
isDarkTheme: _isDarkTheme(context),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
Widget _tapBehaviorButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required bool selectionOnTapEnabled,
|
||||
}) {
|
||||
return _MaterialInspectorButton.toggle(
|
||||
onPressed: onPressed,
|
||||
semanticLabel: semanticLabel,
|
||||
// This icon is also used for the Cupertino-styled button and for DevTools.
|
||||
// It should be updated in all 3 places if changed.
|
||||
icon: CupertinoIcons.cursor_rays,
|
||||
isDarkTheme: _isDarkTheme(context),
|
||||
toggledOn: selectionOnTapEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
bool _isDarkTheme(BuildContext context) {
|
||||
@ -1100,6 +1093,7 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: _moveExitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: _tapBehaviorButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
@ -1135,6 +1129,7 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
|
||||
exitWidgetSelectionButtonBuilder: _exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: _moveExitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: _tapBehaviorButtonBuilder,
|
||||
shortcuts: widget.shortcuts,
|
||||
actions: widget.actions,
|
||||
restorationScopeId: widget.restorationScopeId,
|
||||
@ -1173,3 +1168,101 @@ class _MaterialAppState extends State<MaterialApp> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MaterialInspectorButton extends InspectorButton {
|
||||
const _MaterialInspectorButton.filled({
|
||||
required super.onPressed,
|
||||
required super.semanticLabel,
|
||||
required super.icon,
|
||||
required this.isDarkTheme,
|
||||
super.buttonKey,
|
||||
}) : super.filled();
|
||||
|
||||
const _MaterialInspectorButton.toggle({
|
||||
required super.onPressed,
|
||||
required super.semanticLabel,
|
||||
required super.icon,
|
||||
required this.isDarkTheme,
|
||||
super.toggledOn,
|
||||
}) : super.toggle();
|
||||
|
||||
const _MaterialInspectorButton.iconOnly({
|
||||
required super.onPressed,
|
||||
required super.semanticLabel,
|
||||
required super.icon,
|
||||
required this.isDarkTheme,
|
||||
}) : super.iconOnly();
|
||||
|
||||
final bool isDarkTheme;
|
||||
|
||||
static const EdgeInsets _buttonPadding = EdgeInsets.zero;
|
||||
static const BoxConstraints _buttonConstraints = BoxConstraints.tightFor(
|
||||
width: InspectorButton.buttonSize,
|
||||
height: InspectorButton.buttonSize,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
key: buttonKey,
|
||||
onPressed: onPressed,
|
||||
iconSize: iconSizeForVariant,
|
||||
padding: _buttonPadding,
|
||||
constraints: _buttonConstraints,
|
||||
style: _selectionButtonsIconStyle(context),
|
||||
icon: Icon(icon, semanticLabel: semanticLabel),
|
||||
);
|
||||
}
|
||||
|
||||
ButtonStyle _selectionButtonsIconStyle(BuildContext context) {
|
||||
final Color foreground = foregroundColor(context);
|
||||
final Color background = backgroundColor(context);
|
||||
|
||||
return IconButton.styleFrom(
|
||||
foregroundColor: foreground,
|
||||
backgroundColor: background,
|
||||
side:
|
||||
variant == InspectorButtonVariant.toggle && !toggledOn!
|
||||
? BorderSide(color: foreground)
|
||||
: null,
|
||||
tapTargetSize: MaterialTapTargetSize.padded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Color foregroundColor(BuildContext context) {
|
||||
final Color primaryColor = _primaryColor(context);
|
||||
final Color secondaryColor = _secondaryColor(context);
|
||||
switch (variant) {
|
||||
case InspectorButtonVariant.filled:
|
||||
return primaryColor;
|
||||
case InspectorButtonVariant.iconOnly:
|
||||
return secondaryColor;
|
||||
case InspectorButtonVariant.toggle:
|
||||
return !toggledOn! ? secondaryColor : primaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Color backgroundColor(BuildContext context) {
|
||||
final Color secondaryColor = _secondaryColor(context);
|
||||
switch (variant) {
|
||||
case InspectorButtonVariant.filled:
|
||||
return secondaryColor;
|
||||
case InspectorButtonVariant.iconOnly:
|
||||
return Colors.transparent;
|
||||
case InspectorButtonVariant.toggle:
|
||||
return !toggledOn! ? Colors.transparent : secondaryColor;
|
||||
}
|
||||
}
|
||||
|
||||
Color _primaryColor(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return isDarkTheme ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.primaryContainer;
|
||||
}
|
||||
|
||||
Color _secondaryColor(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return isDarkTheme ? theme.colorScheme.primaryContainer : theme.colorScheme.onPrimaryContainer;
|
||||
}
|
||||
}
|
||||
|
@ -355,6 +355,7 @@ class WidgetsApp extends StatefulWidget {
|
||||
this.debugShowCheckedModeBanner = true,
|
||||
this.exitWidgetSelectionButtonBuilder,
|
||||
this.moveExitWidgetSelectionButtonBuilder,
|
||||
this.tapBehaviorButtonBuilder,
|
||||
this.shortcuts,
|
||||
this.actions,
|
||||
this.restorationScopeId,
|
||||
@ -446,6 +447,7 @@ class WidgetsApp extends StatefulWidget {
|
||||
this.debugShowCheckedModeBanner = true,
|
||||
this.exitWidgetSelectionButtonBuilder,
|
||||
this.moveExitWidgetSelectionButtonBuilder,
|
||||
this.tapBehaviorButtonBuilder,
|
||||
this.shortcuts,
|
||||
this.actions,
|
||||
this.restorationScopeId,
|
||||
@ -1041,19 +1043,27 @@ class WidgetsApp extends StatefulWidget {
|
||||
|
||||
/// Builds the widget the [WidgetInspector] uses to exit selection mode.
|
||||
///
|
||||
/// This lets [MaterialApp] to use a Material Design button to exit the
|
||||
/// inspector select mode without requiring [WidgetInspector] to depend on the
|
||||
/// Material package.
|
||||
/// This lets [MaterialApp] and [CupertinoApp] use an appropriately styled
|
||||
/// button for their design systems without requiring [WidgetInspector] to
|
||||
/// depend on the Material or Cupertino packages.
|
||||
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.
|
||||
/// This lets [MaterialApp] and [CupertinoApp] use an appropriately styled
|
||||
/// button for their design systems without requiring [WidgetInspector] to
|
||||
/// depend on the Material or Cupertino packages.
|
||||
final MoveExitWidgetSelectionButtonBuilder? moveExitWidgetSelectionButtonBuilder;
|
||||
|
||||
/// Builds the widget the [WidgetInspector] uses to change the default
|
||||
/// behavior when tapping on widgets in the app.
|
||||
///
|
||||
/// This lets [MaterialApp] and [CupertinoApp] use an appropriately styled
|
||||
/// button for their design systems without requiring [WidgetInspector] to
|
||||
/// depend on the Material or Cupertino packages.
|
||||
final TapBehaviorButtonBuilder? tapBehaviorButtonBuilder;
|
||||
|
||||
/// {@template flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
|
||||
/// Turns on a little "DEBUG" banner in debug mode to indicate
|
||||
/// that the app is in debug mode. This is on by default (in
|
||||
@ -1824,6 +1834,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
||||
return WidgetInspector(
|
||||
exitWidgetSelectionButtonBuilder: widget.exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: widget.moveExitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: widget.tapBehaviorButtonBuilder,
|
||||
child: child!,
|
||||
);
|
||||
}
|
||||
|
@ -492,6 +492,15 @@ mixin WidgetsBinding
|
||||
_debugShowWidgetInspectorOverrideNotifierObject ??= ValueNotifier<bool>(false);
|
||||
ValueNotifier<bool>? _debugShowWidgetInspectorOverrideNotifierObject;
|
||||
|
||||
/// The notifier for whether or not taps on the device are treated as widget
|
||||
/// selections when the widget inspector is enabled.
|
||||
///
|
||||
/// - If true, taps in the app are intercepted by the widget inspector.
|
||||
/// - If false, taps in the app are not intercepted by the widget inspector.
|
||||
ValueNotifier<bool> get debugWidgetInspectorSelectionOnTapEnabled =>
|
||||
_debugWidgetInspectorSelectionOnTapEnabledNotifierObject ??= ValueNotifier<bool>(true);
|
||||
ValueNotifier<bool>? _debugWidgetInspectorSelectionOnTapEnabledNotifierObject;
|
||||
|
||||
@visibleForTesting
|
||||
@override
|
||||
void resetInternalState() {
|
||||
@ -499,6 +508,8 @@ mixin WidgetsBinding
|
||||
super.resetInternalState();
|
||||
_debugShowWidgetInspectorOverrideNotifierObject?.dispose();
|
||||
_debugShowWidgetInspectorOverrideNotifierObject = null;
|
||||
_debugWidgetInspectorSelectionOnTapEnabledNotifierObject?.dispose();
|
||||
_debugWidgetInspectorSelectionOnTapEnabledNotifierObject = null;
|
||||
}
|
||||
|
||||
void _debugAddStackFilters() {
|
||||
|
@ -34,6 +34,7 @@ import 'binding.dart';
|
||||
import 'debug.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'icon_data.dart';
|
||||
import 'service_extensions.dart';
|
||||
import 'view.dart';
|
||||
|
||||
@ -43,13 +44,29 @@ typedef ExitWidgetSelectionButtonBuilder =
|
||||
Widget Function(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
});
|
||||
|
||||
/// Signature for the builder callback used by
|
||||
/// [WidgetInspector.moveExitWidgetSelectionButtonBuilder].
|
||||
typedef MoveExitWidgetSelectionButtonBuilder =
|
||||
Widget Function(BuildContext context, {required VoidCallback onPressed, bool isLeftAligned});
|
||||
Widget Function(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
bool isLeftAligned,
|
||||
});
|
||||
|
||||
/// Signature for the builder callback used by
|
||||
/// [WidgetInspector.tapBehaviorButtonBuilder].
|
||||
typedef TapBehaviorButtonBuilder =
|
||||
Widget Function(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required bool selectionOnTapEnabled,
|
||||
});
|
||||
|
||||
/// Signature for a method that registers the service extension `callback` with
|
||||
/// the given `name`.
|
||||
@ -2768,6 +2785,7 @@ class WidgetInspector extends StatefulWidget {
|
||||
const WidgetInspector({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.tapBehaviorButtonBuilder,
|
||||
required this.exitWidgetSelectionButtonBuilder,
|
||||
required this.moveExitWidgetSelectionButtonBuilder,
|
||||
});
|
||||
@ -2790,6 +2808,15 @@ class WidgetInspector extends StatefulWidget {
|
||||
/// The button UI should respond to the `leftAligned` argument.
|
||||
final MoveExitWidgetSelectionButtonBuilder? moveExitWidgetSelectionButtonBuilder;
|
||||
|
||||
/// A builder that is called to create the button that changes the default tap
|
||||
/// behavior when Select Widget mode is enabled.
|
||||
///
|
||||
/// The `onPressed` callback passed as an argument to the builder should be
|
||||
/// hooked up to the returned widget.
|
||||
///
|
||||
/// The button UI should respond to the `selectionOnTapEnabled` argument.
|
||||
final TapBehaviorButtonBuilder? tapBehaviorButtonBuilder;
|
||||
|
||||
@override
|
||||
State<WidgetInspector> createState() => _WidgetInspectorState();
|
||||
}
|
||||
@ -2809,6 +2836,11 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
/// as selecting the edge of the bounding box.
|
||||
static const double _edgeHitMargin = 2.0;
|
||||
|
||||
ValueNotifier<bool> get _selectionOnTapEnabled =>
|
||||
WidgetsBinding.instance.debugWidgetInspectorSelectionOnTapEnabled;
|
||||
|
||||
bool get _isSelectModeWithSelectionOnTapEnabled => isSelectMode && _selectionOnTapEnabled.value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -2817,6 +2849,7 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier.addListener(
|
||||
_selectionInformationChanged,
|
||||
);
|
||||
_selectionOnTapEnabled.addListener(_selectionInformationChanged);
|
||||
selection = WidgetInspectorService.instance.selection;
|
||||
isSelectMode = WidgetsBinding.instance.debugShowWidgetInspectorOverride;
|
||||
}
|
||||
@ -2827,6 +2860,7 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier.removeListener(
|
||||
_selectionInformationChanged,
|
||||
);
|
||||
_selectionOnTapEnabled.removeListener(_selectionInformationChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -2911,7 +2945,7 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
}
|
||||
|
||||
void _inspectAt(Offset position) {
|
||||
if (!isSelectMode) {
|
||||
if (!_isSelectModeWithSelectionOnTapEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2952,7 +2986,7 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
if (!isSelectMode) {
|
||||
if (!_isSelectModeWithSelectionOnTapEnabled) {
|
||||
return;
|
||||
}
|
||||
if (_lastPointerLocation != null) {
|
||||
@ -2975,11 +3009,16 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
onPanUpdate: _handlePanUpdate,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
excludeFromSemantics: true,
|
||||
child: IgnorePointer(ignoring: isSelectMode, key: _ignorePointerKey, child: widget.child),
|
||||
child: IgnorePointer(
|
||||
ignoring: _isSelectModeWithSelectionOnTapEnabled,
|
||||
key: _ignorePointerKey,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
_InspectorOverlay(selection: selection),
|
||||
if (isSelectMode && widget.exitWidgetSelectionButtonBuilder != null)
|
||||
_ExitWidgetSelectionButtonGroup(
|
||||
_WidgetInspectorButtonGroup(
|
||||
tapBehaviorButtonBuilder: widget.tapBehaviorButtonBuilder,
|
||||
exitWidgetSelectionButtonBuilder: widget.exitWidgetSelectionButtonBuilder!,
|
||||
moveExitWidgetSelectionButtonBuilder: widget.moveExitWidgetSelectionButtonBuilder,
|
||||
),
|
||||
@ -2988,6 +3027,123 @@ class _WidgetInspectorState extends State<WidgetInspector> with WidgetsBindingOb
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the visual and behavioral variants for an [InspectorButton].
|
||||
enum InspectorButtonVariant {
|
||||
/// A standard button with a filled background and foreground icon.
|
||||
filled,
|
||||
|
||||
/// A button that can be toggled on or off, visually representing its state.
|
||||
///
|
||||
/// The [InspectorButton.toggledOn] property determines its current state.
|
||||
toggle,
|
||||
|
||||
/// A button that displays only an icon, typically with a transparent background.
|
||||
iconOnly,
|
||||
}
|
||||
|
||||
/// An abstract base class for creating Material or Cupertino-styled inspector
|
||||
/// buttons.
|
||||
///
|
||||
/// Subclasses are responsible for implementing the design-specific rendering
|
||||
/// logic in the [build] method and providing design-specific colors via
|
||||
/// [foregroundColor] and [backgroundColor].
|
||||
abstract class InspectorButton extends StatelessWidget {
|
||||
/// Creates an inspector button.
|
||||
///
|
||||
/// This is the base constructor used by named constructors.
|
||||
const InspectorButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.semanticLabel,
|
||||
required this.icon,
|
||||
this.buttonKey,
|
||||
required this.variant,
|
||||
this.toggledOn,
|
||||
});
|
||||
|
||||
/// Creates an inspector button with the [InspectorButtonVariant.filled] style.
|
||||
///
|
||||
/// This button typically has a solid background color and a contrasting icon.
|
||||
const InspectorButton.filled({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.semanticLabel,
|
||||
required this.icon,
|
||||
this.buttonKey,
|
||||
}) : variant = InspectorButtonVariant.filled,
|
||||
toggledOn = null;
|
||||
|
||||
/// Creates an inspector button with the [InspectorButtonVariant.toggle] style.
|
||||
///
|
||||
/// This button can be in an "on" or "off" state, visually indicated.
|
||||
/// The [toggledOn] parameter defaults to `true`.
|
||||
const InspectorButton.toggle({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.semanticLabel,
|
||||
required this.icon,
|
||||
bool this.toggledOn = true,
|
||||
}) : buttonKey = null,
|
||||
variant = InspectorButtonVariant.toggle;
|
||||
|
||||
/// Creates an inspector button with the [InspectorButtonVariant.iconOnly] style.
|
||||
///
|
||||
/// This button typically displays only an icon with a transparent background.
|
||||
const InspectorButton.iconOnly({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
required this.semanticLabel,
|
||||
required this.icon,
|
||||
}) : buttonKey = null,
|
||||
variant = InspectorButtonVariant.iconOnly,
|
||||
toggledOn = null;
|
||||
|
||||
/// The callback that is called when the button is tapped.
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// The semantic label for the button, used for accessibility.
|
||||
final String semanticLabel;
|
||||
|
||||
/// The icon to display within the button.
|
||||
final IconData icon;
|
||||
|
||||
/// An optional key to identify the button widget.
|
||||
final GlobalKey? buttonKey;
|
||||
|
||||
/// The visual and behavioral variant of the button.
|
||||
///
|
||||
/// See [InspectorButtonVariant] for available styles.
|
||||
final InspectorButtonVariant variant;
|
||||
|
||||
/// For [InspectorButtonVariant.toggle] buttons, this determines if the button
|
||||
/// is currently in the "on" (true) or "off" (false) state.
|
||||
final bool? toggledOn;
|
||||
|
||||
/// The standard height and width for the button.
|
||||
static const double buttonSize = 32.0;
|
||||
|
||||
/// The standard size for the icon when it's not the only element (e.g., in filled or toggle buttons).
|
||||
///
|
||||
/// For [InspectorButtonVariant.iconOnly], the icon typically takes up the full [buttonSize].
|
||||
static const double buttonIconSize = 18.0;
|
||||
|
||||
/// Gets the appropriate icon size based on the button's [variant].
|
||||
///
|
||||
/// Returns [buttonSize] if the variant is [InspectorButtonVariant.iconOnly],
|
||||
/// otherwise returns [buttonIconSize].
|
||||
double get iconSizeForVariant =>
|
||||
variant == InspectorButtonVariant.iconOnly ? buttonSize : buttonIconSize;
|
||||
|
||||
/// Provides the appropriate foreground color for the button's icon.
|
||||
Color foregroundColor(BuildContext context);
|
||||
|
||||
/// Provides the appropriate background color for the button.
|
||||
Color backgroundColor(BuildContext context);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context);
|
||||
}
|
||||
|
||||
/// Mutable selection state of the inspector.
|
||||
class InspectorSelection with ChangeNotifier {
|
||||
/// Creates an instance of [InspectorSelection].
|
||||
@ -3455,22 +3611,24 @@ const double _kOffScreenMargin = 1.0;
|
||||
|
||||
const TextStyle _messageStyle = TextStyle(color: Color(0xFFFFFFFF), fontSize: 10.0, height: 1.2);
|
||||
|
||||
class _ExitWidgetSelectionButtonGroup extends StatefulWidget {
|
||||
const _ExitWidgetSelectionButtonGroup({
|
||||
class _WidgetInspectorButtonGroup extends StatefulWidget {
|
||||
const _WidgetInspectorButtonGroup({
|
||||
required this.exitWidgetSelectionButtonBuilder,
|
||||
required this.moveExitWidgetSelectionButtonBuilder,
|
||||
required this.tapBehaviorButtonBuilder,
|
||||
});
|
||||
|
||||
final ExitWidgetSelectionButtonBuilder exitWidgetSelectionButtonBuilder;
|
||||
final MoveExitWidgetSelectionButtonBuilder? moveExitWidgetSelectionButtonBuilder;
|
||||
final TapBehaviorButtonBuilder? tapBehaviorButtonBuilder;
|
||||
|
||||
@override
|
||||
State<_ExitWidgetSelectionButtonGroup> createState() => _ExitWidgetSelectionButtonGroupState();
|
||||
State<_WidgetInspectorButtonGroup> createState() => _WidgetInspectorButtonGroupState();
|
||||
}
|
||||
|
||||
class _ExitWidgetSelectionButtonGroupState extends State<_ExitWidgetSelectionButtonGroup> {
|
||||
static const double _kExitWidgetSelectionButtonPadding = 4.0;
|
||||
class _WidgetInspectorButtonGroupState extends State<_WidgetInspectorButtonGroup> {
|
||||
static const double _kExitWidgetSelectionButtonMargin = 10.0;
|
||||
static const bool _defaultSelectionOnTapEnabled = true;
|
||||
|
||||
final GlobalKey _exitWidgetSelectionButtonKey = GlobalKey(
|
||||
debugLabel: 'Exit Widget Selection button',
|
||||
@ -3480,31 +3638,78 @@ class _ExitWidgetSelectionButtonGroupState extends State<_ExitWidgetSelectionBut
|
||||
|
||||
bool _leftAligned = true;
|
||||
|
||||
ValueNotifier<bool> get _selectionOnTapEnabled =>
|
||||
WidgetsBinding.instance.debugWidgetInspectorSelectionOnTapEnabled;
|
||||
|
||||
Widget? get _moveExitWidgetSelectionButton {
|
||||
final MoveExitWidgetSelectionButtonBuilder? buttonBuilder =
|
||||
widget.moveExitWidgetSelectionButtonBuilder;
|
||||
if (buttonBuilder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String buttonLabel = 'Move to the ${_leftAligned ? 'right' : 'left'}';
|
||||
return _WidgetInspectorButton(
|
||||
button: buttonBuilder(
|
||||
context,
|
||||
onPressed: () {
|
||||
_changeButtonGroupAlignment();
|
||||
_onTooltipHidden();
|
||||
},
|
||||
semanticLabel: buttonLabel,
|
||||
isLeftAligned: _leftAligned,
|
||||
),
|
||||
onTooltipVisible: () {
|
||||
_changeTooltipMessage(buttonLabel);
|
||||
},
|
||||
onTooltipHidden: _onTooltipHidden,
|
||||
);
|
||||
}
|
||||
|
||||
Widget get _exitWidgetSelectionButton {
|
||||
const String buttonLabel = 'Exit Select Widget mode';
|
||||
return _WidgetInspectorButton(
|
||||
button: widget.exitWidgetSelectionButtonBuilder(
|
||||
context,
|
||||
onPressed: _exitWidgetSelectionMode,
|
||||
semanticLabel: buttonLabel,
|
||||
key: _exitWidgetSelectionButtonKey,
|
||||
),
|
||||
onTooltipVisible: () {
|
||||
_changeTooltipMessage(buttonLabel);
|
||||
},
|
||||
onTooltipHidden: _onTooltipHidden,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? get _tapBehaviorButton {
|
||||
final TapBehaviorButtonBuilder? buttonBuilder = widget.tapBehaviorButtonBuilder;
|
||||
if (buttonBuilder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _WidgetInspectorButton(
|
||||
button: buttonBuilder(
|
||||
context,
|
||||
onPressed: _changeSelectionOnTapMode,
|
||||
semanticLabel: 'Change widget selection mode for taps',
|
||||
selectionOnTapEnabled: _selectionOnTapEnabled.value,
|
||||
),
|
||||
onTooltipVisible: _changeSelectionOnTapTooltip,
|
||||
onTooltipHidden: _onTooltipHidden,
|
||||
);
|
||||
}
|
||||
|
||||
bool get _tooltipVisible => _tooltipMessage != null;
|
||||
|
||||
@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 selectionModeButtons = Column(
|
||||
children: <Widget>[
|
||||
if (_tapBehaviorButton != null) _tapBehaviorButton!,
|
||||
_exitWidgetSelectionButton,
|
||||
],
|
||||
);
|
||||
|
||||
final Widget buttonGroup = Stack(
|
||||
alignment: AlignmentDirectional.topCenter,
|
||||
@ -3517,22 +3722,12 @@ class _ExitWidgetSelectionButtonGroupState extends State<_ExitWidgetSelectionBut
|
||||
),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
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,
|
||||
if (_leftAligned) selectionModeButtons,
|
||||
if (_moveExitWidgetSelectionButton != null) _moveExitWidgetSelectionButton!,
|
||||
if (!_leftAligned) selectionModeButtons,
|
||||
],
|
||||
),
|
||||
],
|
||||
@ -3548,6 +3743,25 @@ class _ExitWidgetSelectionButtonGroupState extends State<_ExitWidgetSelectionBut
|
||||
|
||||
void _exitWidgetSelectionMode() {
|
||||
WidgetInspectorService.instance._changeWidgetSelectionMode(false);
|
||||
// Reset to default selection on tap behavior on exit.
|
||||
_changeSelectionOnTapMode(selectionOnTapEnabled: _defaultSelectionOnTapEnabled);
|
||||
}
|
||||
|
||||
void _changeSelectionOnTapMode({bool? selectionOnTapEnabled}) {
|
||||
final bool newValue = selectionOnTapEnabled ?? !_selectionOnTapEnabled.value;
|
||||
_selectionOnTapEnabled.value = newValue;
|
||||
WidgetInspectorService.instance.selection.clear();
|
||||
if (_tooltipVisible) {
|
||||
_changeSelectionOnTapTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
void _changeSelectionOnTapTooltip() {
|
||||
_changeTooltipMessage(
|
||||
_selectionOnTapEnabled.value
|
||||
? 'Disable widget selection for taps'
|
||||
: 'Enable widget selection for taps',
|
||||
);
|
||||
}
|
||||
|
||||
void _changeButtonGroupAlignment() {
|
||||
@ -3571,8 +3785,8 @@ class _ExitWidgetSelectionButtonGroupState extends State<_ExitWidgetSelectionBut
|
||||
}
|
||||
}
|
||||
|
||||
class _TooltipGestureDetector extends StatefulWidget {
|
||||
const _TooltipGestureDetector({
|
||||
class _WidgetInspectorButton extends StatefulWidget {
|
||||
const _WidgetInspectorButton({
|
||||
required this.button,
|
||||
required this.onTooltipVisible,
|
||||
required this.onTooltipHidden,
|
||||
@ -3586,10 +3800,10 @@ class _TooltipGestureDetector extends StatefulWidget {
|
||||
static const Duration _tooltipDelayDuration = Duration(milliseconds: 100);
|
||||
|
||||
@override
|
||||
State<_TooltipGestureDetector> createState() => _TooltipGestureDetectorState();
|
||||
State<_WidgetInspectorButton> createState() => _WidgetInspectorButtonState();
|
||||
}
|
||||
|
||||
class _TooltipGestureDetectorState extends State<_TooltipGestureDetector> {
|
||||
class _WidgetInspectorButtonState extends State<_WidgetInspectorButton> {
|
||||
Timer? _tooltipVisibleTimer;
|
||||
Timer? _tooltipHiddenTimer;
|
||||
|
||||
@ -3609,18 +3823,18 @@ class _TooltipGestureDetectorState extends State<_TooltipGestureDetector> {
|
||||
children: <Widget>[
|
||||
GestureDetector(
|
||||
onLongPress: () {
|
||||
_tooltipVisibleAfter(_TooltipGestureDetector._tooltipDelayDuration);
|
||||
_tooltipVisibleAfter(_WidgetInspectorButton._tooltipDelayDuration);
|
||||
_tooltipHiddenAfter(
|
||||
_TooltipGestureDetector._tooltipShownOnLongPressDuration +
|
||||
_TooltipGestureDetector._tooltipDelayDuration,
|
||||
_WidgetInspectorButton._tooltipShownOnLongPressDuration +
|
||||
_WidgetInspectorButton._tooltipDelayDuration,
|
||||
);
|
||||
},
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
_tooltipVisibleAfter(_TooltipGestureDetector._tooltipDelayDuration);
|
||||
_tooltipVisibleAfter(_WidgetInspectorButton._tooltipDelayDuration);
|
||||
},
|
||||
onExit: (_) {
|
||||
_tooltipHiddenAfter(_TooltipGestureDetector._tooltipDelayDuration);
|
||||
_tooltipHiddenAfter(_WidgetInspectorButton._tooltipDelayDuration);
|
||||
},
|
||||
child: widget.button,
|
||||
),
|
||||
|
@ -343,6 +343,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: WidgetInspector(
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Text('a', textDirection: TextDirection.ltr),
|
||||
@ -370,6 +371,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
exitWidgetSelectionButtonKey = key;
|
||||
@ -422,6 +424,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: Material(
|
||||
child: ListView(
|
||||
children: <Widget>[
|
||||
@ -515,6 +518,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: WidgetInspector(
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(0.0),
|
||||
child: const Stack(
|
||||
@ -545,6 +549,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
exitWidgetSelectionButtonKey = key;
|
||||
@ -558,6 +563,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: ListView(
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
children: <Widget>[Container(key: childKey, height: 5000.0)],
|
||||
@ -621,6 +627,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: WidgetInspector(
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
expect(didLongPress, isFalse);
|
||||
@ -688,6 +695,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
entry1 = OverlayEntry(
|
||||
@ -747,6 +755,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: WidgetInspector(
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: ColoredBox(
|
||||
color: Colors.white,
|
||||
child: Center(
|
||||
@ -791,7 +800,12 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
final GlobalKey child2Key = GlobalKey();
|
||||
|
||||
ExitWidgetSelectionButtonBuilder exitWidgetSelectionButtonBuilder(Key key) {
|
||||
return (BuildContext context, {required VoidCallback onPressed, required GlobalKey key}) {
|
||||
return (
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
};
|
||||
}
|
||||
@ -813,6 +827,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
selectButton1Key,
|
||||
),
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: Container(key: child1Key, child: const Text('Child 1')),
|
||||
),
|
||||
),
|
||||
@ -823,6 +838,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
selectButton2Key,
|
||||
),
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: Container(key: child2Key, child: const Text('Child 2')),
|
||||
),
|
||||
),
|
||||
@ -858,6 +874,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
@ -869,6 +886,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: const Text('Child 1'),
|
||||
),
|
||||
@ -918,6 +936,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(
|
||||
@ -932,6 +951,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
Widget moveWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
bool isLeftAligned = true,
|
||||
}) {
|
||||
return Material(
|
||||
@ -953,6 +973,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: moveWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: const Text('APP'),
|
||||
),
|
||||
),
|
||||
@ -990,6 +1011,118 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(),
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'WidgetInspector Tap behavior button',
|
||||
(WidgetTester tester) async {
|
||||
Widget exitWidgetSelectionButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required GlobalKey key,
|
||||
}) {
|
||||
return Material(child: ElevatedButton(onPressed: onPressed, key: key, child: null));
|
||||
}
|
||||
|
||||
Widget tapBehaviorButtonBuilder(
|
||||
BuildContext context, {
|
||||
required VoidCallback onPressed,
|
||||
required String semanticLabel,
|
||||
required bool selectionOnTapEnabled,
|
||||
}) {
|
||||
return Material(
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
child: Text(selectionOnTapEnabled ? 'SELECTION ON TAP' : 'APP INTERACTION ON TAP'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Finder buttonFinder(String buttonText) {
|
||||
return find.ancestor(of: find.text(buttonText), matching: find.byType(ElevatedButton));
|
||||
}
|
||||
|
||||
int navigateEventsCount() =>
|
||||
service.dispatchedEvents('navigate', stream: 'ToolEvent').length;
|
||||
|
||||
// Enable widget selection mode.
|
||||
WidgetInspectorService.instance.isSelectMode = true;
|
||||
|
||||
// Pump the test widget.
|
||||
final GlobalKey inspectorKey = GlobalKey();
|
||||
setupDefaultPubRootDirectory(service);
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: WidgetInspector(
|
||||
key: inspectorKey,
|
||||
exitWidgetSelectionButtonBuilder: exitWidgetSelectionButtonBuilder,
|
||||
tapBehaviorButtonBuilder: tapBehaviorButtonBuilder,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
child: const Row(children: <Widget>[Text('Child 1'), Text('Child 2')]),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Verify there are no navigate events yet.
|
||||
expect(navigateEventsCount(), equals(0));
|
||||
|
||||
// Tap on the first child widget.
|
||||
final Finder child1 = find.text('Child 1');
|
||||
await tester.tap(child1, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the selection matches the first child widget.
|
||||
final Element child1Element = child1.evaluate().first;
|
||||
expect(service.selection.current, equals(child1Element.renderObject));
|
||||
|
||||
// Verify that a navigate event was sent.
|
||||
expect(navigateEventsCount(), equals(1));
|
||||
|
||||
// Tap on the SELECTION ON TAP button.
|
||||
final Finder tapBehaviorButtonBefore = buttonFinder('SELECTION ON TAP');
|
||||
expect(tapBehaviorButtonBefore, findsOneWidget);
|
||||
await tester.tap(tapBehaviorButtonBefore);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the tap behavior button's UI has been updated.
|
||||
expect(tapBehaviorButtonBefore, findsNothing);
|
||||
final Finder tapBehaviorButtonAfter = buttonFinder('APP INTERACTION ON TAP');
|
||||
expect(tapBehaviorButtonAfter, findsOneWidget);
|
||||
|
||||
// Tap on the second child widget.
|
||||
final Finder child2 = find.text('Child 2');
|
||||
await tester.tap(child2, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
// Verify there is no selection.
|
||||
expect(service.selection.current, isNull);
|
||||
|
||||
// Verify no navigate events were sent.
|
||||
expect(navigateEventsCount(), equals(1));
|
||||
|
||||
// Tap on the SELECTION ON TAP button again.
|
||||
await tester.tap(tapBehaviorButtonAfter);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the tap behavior button's UI has been reset.
|
||||
expect(tapBehaviorButtonAfter, findsNothing);
|
||||
expect(tapBehaviorButtonBefore, findsOneWidget);
|
||||
|
||||
// Tap on the second child widget again.
|
||||
await tester.tap(child2, warnIfMissed: false);
|
||||
await tester.pump();
|
||||
|
||||
// Verify the selection now matches the second child widget.
|
||||
final Element child2Element = child2.evaluate().first;
|
||||
expect(service.selection.current, equals(child2Element.renderObject));
|
||||
|
||||
// Verify another navigate event was sent.
|
||||
expect(navigateEventsCount(), equals(2));
|
||||
},
|
||||
// [intended] Test requires --track-widget-creation flag.
|
||||
skip: !WidgetInspectorService.instance.isWidgetCreationTracked(),
|
||||
);
|
||||
|
||||
testWidgets('test transformDebugCreator will re-order if after stack trace', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -3867,6 +4000,7 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
|
||||
child: WidgetInspector(
|
||||
exitWidgetSelectionButtonBuilder: null,
|
||||
moveExitWidgetSelectionButtonBuilder: null,
|
||||
tapBehaviorButtonBuilder: null,
|
||||
child: _applyConstructor(_TrivialWidget.new),
|
||||
),
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user