[Inspector V2] Toggle button hides implementation widgets in the widget tree (#8143)
diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart index 6cc0fb4..3689324 100644 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controller.dart
@@ -216,6 +216,11 @@ (widgetProperties: [], renderProperties: []), ); + /// Whether the implementation widgets are hidden in the widget tree. + ValueListenable<bool> get implementationWidgetsHidden => + _implementationWidgetsHidden; + final _implementationWidgetsHidden = ValueNotifier<bool>(false); + InspectorTreeNode? lastExpanded; bool isActive = false; @@ -403,7 +408,10 @@ } } - Future<void> _recomputeTreeRoot(RemoteDiagnosticsNode? newSelection) async { + Future<void> _recomputeTreeRoot( + RemoteDiagnosticsNode? newSelection, { + bool hideImplementationWidgets = false, + }) async { assert(!_disposed); final treeGroups = _treeGroups; if (_disposed || treeGroups == null) { @@ -413,7 +421,10 @@ treeGroups.cancelNext(); try { final group = treeGroups.next; - final node = await group.getRoot(treeType); + final node = await group.getRoot( + treeType, + isSummaryTree: hideImplementationWidgets, + ); if (node == null || group.disposed || _disposed) { return; } @@ -431,6 +442,7 @@ inspectorTree.root = rootNode; refreshSelection(newSelection); + _implementationWidgetsHidden.value = hideImplementationWidgets; } catch (error, st) { _log.shout(error, error, st); treeGroups.cancelNext(); @@ -438,6 +450,19 @@ } } + Future<void> toggleImplementationWidgetsVisibility() async { + final root = inspectorTree.root?.diagnostic; + if (root != null) { + final currentSelectedNode = selectedNode.value; + await _recomputeTreeRoot( + root, + hideImplementationWidgets: !_implementationWidgetsHidden.value, + ); + // Persist the selected node after refreshing the widget tree: + refreshSelection(currentSelectedNode?.diagnostic); + } + } + void _clearValueToInspectorTreeNodeMapping() { valueToInspectorTreeNode.clear(); } @@ -473,10 +498,13 @@ void refreshSelection(RemoteDiagnosticsNode? newSelection) { newSelection ??= selectedDiagnostic; - setSelectedNode(findMatchingInspectorTreeNode(newSelection)); - syncSelectionHelper(selection: newSelection); + final matchingNode = findMatchingInspectorTreeNode(newSelection); + if (matchingNode != null) { + setSelectedNode(matchingNode); + syncSelectionHelper(selection: newSelection); - syncTreeSelection(); + syncTreeSelection(); + } } void syncTreeSelection() {
diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart new file mode 100644 index 0000000..b115722 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart
@@ -0,0 +1,128 @@ +// Copyright 2024 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:devtools_app_shared/ui.dart'; +import 'package:flutter/material.dart'; + +import '../../service/service_extension_widgets.dart'; +import '../../service/service_extensions.dart' as extensions; +import '../../shared/analytics/constants.dart' as gac; +import '../../shared/common_widgets.dart'; +import '../../shared/globals.dart'; +import 'inspector_controller.dart'; +import 'inspector_screen.dart'; + +/// Control buttons for the inspector panel. +class InspectorControls extends StatelessWidget { + const InspectorControls({super.key, required this.controller}); + + final InspectorController controller; + + static const serviceExtensionButtonsIncludeTextWidth = 1200.0; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ValueListenableBuilder<bool>( + valueListenable: serviceConnection + .serviceManager.serviceExtensionManager + .hasServiceExtension( + extensions.toggleSelectWidgetMode.extension, + ), + builder: (_, selectModeSupported, __) { + return ServiceExtensionButtonGroup( + extensions: [ + selectModeSupported + ? extensions.toggleSelectWidgetMode + : extensions.toggleOnDeviceWidgetInspector, + ], + minScreenWidthForTextBeforeScaling: + InspectorScreenBodyState.minScreenWidthForTextBeforeScaling, + ); + }, + ), + const SizedBox(width: defaultSpacing), + ShowImplementationWidgetsButton(controller: controller), + const Spacer(), + const SizedBox(width: defaultSpacing), + const InspectorServiceExtensionButtonGroup(), + ], + ); + } +} + +/// Group of service extension buttons for the inspector panel that control the +/// overlays painted on the connected app. +class InspectorServiceExtensionButtonGroup extends StatelessWidget { + const InspectorServiceExtensionButtonGroup({super.key}); + + static const serviceExtensionButtonsIncludeTextWidth = 1200.0; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + ServiceExtensionButtonGroup( + minScreenWidthForTextBeforeScaling: + serviceExtensionButtonsIncludeTextWidth, + extensions: [ + extensions.slowAnimations, + extensions.debugPaint, + extensions.debugPaintBaselines, + extensions.repaintRainbow, + extensions.invertOversizedImages, + ], + ), + const SizedBox(width: defaultSpacing), + SettingsOutlinedButton( + gaScreen: gac.inspector, + gaSelection: gac.inspectorSettings, + tooltip: 'Flutter Inspector Settings', + onPressed: () { + unawaited( + showDialog( + context: context, + builder: (context) => const FlutterInspectorSettingsDialog(), + ), + ); + }, + ), + ], + ); + } +} + +/// Toggle button that allows showing/hiding the implementation widgets in the +/// widget tree. +class ShowImplementationWidgetsButton extends StatelessWidget { + const ShowImplementationWidgetsButton({ + super.key, + required this.controller, + }); + + final InspectorController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.implementationWidgetsHidden, + builder: (context, isHidden, _) { + return DevToolsToggleButton( + isSelected: !isHidden, + message: + 'Show widgets created by the Flutter framework or other packages.', + label: 'Show Implementation Widgets', + onPressed: controller.toggleImplementationWidgetsVisibility, + icon: Icons.code, + minScreenWidthForTextBeforeScaling: + InspectorScreenBodyState.minScreenWidthForTextBeforeScaling, + ); + }, + ); + } +}
diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart index 4468685..38141ec 100644 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart
@@ -11,8 +11,6 @@ import 'package:flutter/material.dart'; import 'package:vm_service/vm_service.dart' hide Stack; -import '../../service/service_extension_widgets.dart'; -import '../../service/service_extensions.dart' as extensions; import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/common_widgets.dart'; @@ -26,6 +24,7 @@ import '../../shared/ui/search.dart'; import '../../shared/utils.dart'; import 'inspector_controller.dart'; +import 'inspector_controls.dart'; import 'inspector_tree_controller.dart'; import 'widget_details.dart'; @@ -76,7 +75,6 @@ static const inspectorTreeKey = Key('Inspector Tree'); static const minScreenWidthForTextBeforeScaling = 900.0; - static const serviceExtensionButtonsIncludeTextWidth = 1200.0; @override void dispose() { @@ -149,31 +147,7 @@ ); return Column( children: <Widget>[ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ValueListenableBuilder<bool>( - valueListenable: serviceConnection - .serviceManager.serviceExtensionManager - .hasServiceExtension( - extensions.toggleSelectWidgetMode.extension, - ), - builder: (_, selectModeSupported, __) { - return ServiceExtensionButtonGroup( - extensions: [ - selectModeSupported - ? extensions.toggleSelectWidgetMode - : extensions.toggleOnDeviceWidgetInspector, - ], - minScreenWidthForTextBeforeScaling: - minScreenWidthForTextBeforeScaling, - ); - }, - ), - const Spacer(), - Row(children: getServiceExtensionWidgets()), - ], - ), + InspectorControls(controller: controller), const SizedBox(height: intermediateSpacing), Expanded( child: widgetTrees, @@ -252,38 +226,6 @@ _inspectorTreeController.resetSearch(); } - List<Widget> getServiceExtensionWidgets() { - return [ - ServiceExtensionButtonGroup( - minScreenWidthForTextBeforeScaling: - serviceExtensionButtonsIncludeTextWidth, - extensions: [ - extensions.slowAnimations, - extensions.debugPaint, - extensions.debugPaintBaselines, - extensions.repaintRainbow, - extensions.invertOversizedImages, - ], - ), - const SizedBox(width: defaultSpacing), - SettingsOutlinedButton( - gaScreen: gac.inspector, - gaSelection: gac.inspectorSettings, - tooltip: 'Flutter Inspector Settings', - onPressed: () { - unawaited( - showDialog( - context: context, - builder: (context) => const FlutterInspectorSettingsDialog(), - ), - ); - }, - ), - // TODO(jacobr): implement TogglePlatformSelector. - // TogglePlatformSelector().selector - ]; - } - void _refreshInspector() { ga.select(gac.inspector, gac.refresh); unawaited(
diff --git a/packages/devtools_app/lib/src/shared/console/widgets/description.dart b/packages/devtools_app/lib/src/shared/console/widgets/description.dart index 4fd282a..0a415b5 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/description.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/description.dart
@@ -414,7 +414,8 @@ // Grey out nodes that were not created by the local project to emphasize // those that were: if (emphasizeNodesFromLocalProject && - !diagnosticLocal.isCreatedByLocalProject) { + !diagnosticLocal.isCreatedByLocalProject && + diagnosticLocal.description != '[root]') { textStyle = textStyle.merge(theme.subtleTextStyle); }
diff --git a/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart b/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart index 7168608..c6add98 100644 --- a/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart +++ b/packages/devtools_app/test/inspector_v2/inspector_integration_test.dart
@@ -6,6 +6,7 @@ hide InspectorScreen, InspectorScreenBodyState, InspectorScreenBody; import 'package:devtools_app/src/screens/inspector_v2/inspector_screen.dart'; import 'package:devtools_app/src/screens/inspector_v2/widget_properties/properties_view.dart'; +import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_test/helpers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -49,6 +50,10 @@ await env.setupEnvironment(); }); + tearDown(() async { + await env.tearDownEnvironment(); + }); + tearDownAll(() async { await env.tearDownEnvironment(force: true); }); @@ -71,8 +76,6 @@ '../test_infra/goldens/integration_inspector_v2_initial_load.png', ), ); - - await env.tearDownEnvironment(); }, ); @@ -111,8 +114,6 @@ value: '[Directionality]', tester: tester, ); - - await env.tearDownEnvironment(); }, ); @@ -168,8 +169,6 @@ '../test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png', ), ); - - await env.tearDownEnvironment(); }, ); @@ -206,12 +205,46 @@ '../test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png', ), ); - - await env.tearDownEnvironment(); }, ); }); + testWidgetsWithWindowSize( + 'hide all implementation widgets', + windowSize, + (WidgetTester tester) async { + await _loadInspectorUI(tester); + + // Give time for the initial animation to complete. + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Confirm the hidden widgets are visible behind affordances like "X more + // widgets". + expect( + find.richTextContaining('more widgets...'), + findsWidgets, + ); + + // Tap the "Show Implementation Widgets" button (selected by default). + final showImplementationWidgetsButton = find.descendant( + of: find.byType(DevToolsToggleButton), + matching: find.text('Show Implementation Widgets'), + ); + expect(showImplementationWidgetsButton, findsOneWidget); + await tester.tap(showImplementationWidgetsButton); + await tester.pumpAndSettle(inspectorChangeSettleTime); + + // Confirm that the hidden widgets are no longer visible. + expect(find.richTextContaining('more widgets...'), findsNothing); + await expectLater( + find.byType(InspectorScreenBody), + matchesDevToolsGolden( + '../test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png', + ), + ); + }, + ); + group('widget errors', () { testWidgetsWithWindowSize( 'show navigator and error labels', @@ -262,8 +295,6 @@ '../test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png', ), ); - - await env.tearDownEnvironment(); }, ); });
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png index b43556e..061da41 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_1_initial_load.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png index c0ecbe1..c14c312 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_errors_2_error_selected.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png index 20fa6d5..038ff2e 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png index 20fa6d5..038ff2e 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_hideable_widget_selected_from_search.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png index 01eed6c..ec04838 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_collapsed.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png index fdbb441..13c4e2b 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_expanded.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png new file mode 100644 index 0000000..a1d0c15 --- /dev/null +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_implementation_widgets_hidden.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png index 4cf41d5..fde40a7 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_initial_load.png Binary files differ
diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png index 115415b..d832c94 100644 --- a/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png +++ b/packages/devtools_app/test/test_infra/goldens/integration_inspector_v2_select_center.png Binary files differ
diff --git a/packages/devtools_app_shared/lib/src/ui/buttons.dart b/packages/devtools_app_shared/lib/src/ui/buttons.dart index 12e3072..d991589 100644 --- a/packages/devtools_app_shared/lib/src/ui/buttons.dart +++ b/packages/devtools_app_shared/lib/src/ui/buttons.dart
@@ -213,6 +213,7 @@ this.outlined = true, this.label, this.shape, + this.minScreenWidthForTextBeforeScaling, }); final String message; @@ -229,6 +230,8 @@ final bool outlined; + final double? minScreenWidthForTextBeforeScaling; + @override Widget build(BuildContext context) { return DevToolsToggleButtonGroup( @@ -245,6 +248,8 @@ child: MaterialIconLabel( iconData: icon, label: label, + minScreenWidthForTextBeforeScaling: + minScreenWidthForTextBeforeScaling, ), ), ),