From 44fb121faca3b86b9f38c074f181c7ce8709a612 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:05:49 -0700 Subject: [PATCH 01/10] Initial deletion of legacy inspector --- .../test/live_connection/eval_utils.dart | 2 +- packages/devtools_app/lib/devtools_app.dart | 8 +- .../inspector/inspector_breadcrumbs.dart | 201 --- .../inspector/inspector_controller.dart | 950 ------------ .../inspector/inspector_data_models.dart | 887 ----------- .../inspector/inspector_screen_body.dart | 458 ------ .../inspector_screen_details_tab.dart | 143 -- .../inspector/inspector_tree_controller.dart | 1331 ----------------- .../inspector/layout_explorer/box/box.dart | 446 ------ .../inspector/layout_explorer/flex/flex.dart | 777 ---------- .../inspector/layout_explorer/flex/utils.dart | 198 --- .../layout_explorer/layout_explorer.dart | 71 - .../inspector/layout_explorer/ui/arrow.dart | 280 ---- .../layout_explorer/ui/dimension.dart | 37 - .../layout_explorer/ui/free_space.dart | 156 -- .../ui/layout_explorer_widget.dart | 296 ---- .../ui/overflow_indicator_painter.dart | 63 - .../inspector/layout_explorer/ui/theme.dart | 139 -- .../inspector/layout_explorer/ui/utils.dart | 439 ------ .../ui/widget_constraints.dart | 177 --- .../layout_explorer/ui/widgets_theme.dart | 250 ---- .../inspector_shared/inspector_screen.dart | 61 +- .../inspector_screen_controller.dart | 41 +- .../inspector_settings_dialog.dart | 192 +-- .../inspector_v2/inspector_data_models.dart | 2 +- .../layout_explorer/ui/utils.dart | 2 +- .../screens/logging/logging_controller.dart | 2 +- .../lib/src/shared/analytics/constants.dart | 4 +- .../lib/src/shared/analytics/metrics.dart | 8 +- .../shared/console/eval/inspector_tree.dart | 301 ---- .../shared/console/widgets/description.dart | 4 +- .../preferences/_inspector_preferences.dart | 80 - .../devtools_app/lib/src/shared/ui/icons.dart | 2 +- .../macos/Runner.xcodeproj/project.pbxproj | 86 ++ .../screens/inspector/diagnostics_test.dart | 301 ---- .../inspector_error_navigator_test.dart | 131 -- .../inspector/inspector_integration_test.dart | 483 ------ .../inspector/inspector_screen_test.dart | 374 ----- .../inspector/inspector_tree_test.dart | 175 --- .../layout_explorer/flex/arrow_test.dart | 109 -- .../layout_explorer/flex/flex_test.dart | 292 ---- .../inspector_data_models_test.dart | 442 ------ ...ayout_explorer_serialization_delegate.dart | 64 - .../layout_explorer_test_utils.dart | 28 - .../layout_explorer/widget_theme_test.dart | 22 - .../inspector/utils/inspector_tree.dart | 59 - .../inspector_error_navigator_test.dart | 1 + .../inspector_integration_test.dart | 98 +- .../inspector_v2/inspector_screen_test.dart | 14 +- .../inspector_v2/inspector_tree_test.dart | 10 +- .../layout_explorer/flex/flex_test.dart | 4 +- .../inspector_v2/utils/inspector_tree.dart | 5 +- .../devtools_test/lib/src/mocks/mocks.dart | 21 +- 53 files changed, 176 insertions(+), 10551 deletions(-) delete mode 100644 packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart delete mode 100644 packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart delete mode 100644 packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart delete mode 100644 packages/devtools_app/test/screens/inspector/diagnostics_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/inspector_integration_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/inspector_screen_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/inspector_tree_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/layout_explorer/flex/arrow_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/layout_explorer/inspector_data_models_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_serialization_delegate.dart delete mode 100644 packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_test_utils.dart delete mode 100644 packages/devtools_app/test/screens/inspector/layout_explorer/widget_theme_test.dart delete mode 100644 packages/devtools_app/test/screens/inspector/utils/inspector_tree.dart diff --git a/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart b/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart index 83043cdb869..7e554e86bea 100644 --- a/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart +++ b/packages/devtools_app/integration_test/test/live_connection/eval_utils.dart @@ -73,7 +73,7 @@ class EvalTester { Future selectWidgetTreeNode(Finder finder) async { await tapAndPump( find.descendant( - of: find.byKey(InspectorScreenBodyState.summaryTreeKey), + of: find.byKey(InspectorScreenBodyState.inspectorTreeKey), matching: finder, ), ); diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index 5917216e754..c219f4721c9 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -24,11 +24,11 @@ export 'src/screens/deep_link_validation/deep_links_controller.dart'; export 'src/screens/deep_link_validation/deep_links_screen.dart'; export 'src/screens/dtd/dtd_tools_controller.dart'; export 'src/screens/dtd/dtd_tools_screen.dart'; -export 'src/screens/inspector/inspector_controller.dart'; -export 'src/screens/inspector/inspector_screen_body.dart'; -export 'src/screens/inspector/inspector_tree_controller.dart'; export 'src/screens/inspector_shared/inspector_screen.dart'; export 'src/screens/inspector_shared/inspector_screen_controller.dart'; +export 'src/screens/inspector_v2/inspector_controller.dart'; +export 'src/screens/inspector_v2/inspector_screen_body.dart'; +export 'src/screens/inspector_v2/inspector_tree_controller.dart'; export 'src/screens/logging/log_details_controller.dart'; export 'src/screens/logging/logging_controller.dart'; export 'src/screens/logging/logging_screen.dart'; @@ -76,7 +76,7 @@ export 'src/shared/analytics/analytics_controller.dart'; export 'src/shared/charts/treemap.dart'; export 'src/shared/console/console_service.dart'; export 'src/shared/console/eval/eval_service.dart'; -export 'src/shared/console/eval/inspector_tree.dart'; +export 'src/shared/console/eval/inspector_tree_v2.dart'; export 'src/shared/console/primitives/simple_items.dart'; export 'src/shared/console/widgets/description.dart'; export 'src/shared/diagnostics/diagnostics_node.dart'; diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart deleted file mode 100644 index f5db041746e..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_breadcrumbs.dart +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../shared/console/eval/inspector_tree.dart'; -import '../../shared/primitives/diagnostics_text_styles.dart'; -import '../../shared/primitives/utils.dart'; -import '../../shared/ui/common_widgets.dart'; - -class InspectorBreadcrumbNavigator extends StatelessWidget { - const InspectorBreadcrumbNavigator({ - super.key, - required this.items, - required this.onTap, - }); - - /// Max number of visible breadcrumbs including root item but not 'more' item. - /// E.g. value 5 means root and 4 breadcrumbs can be displayed, other - /// breadcrumbs (if any) will be replaced by '...' item. - static const _maxNumberOfBreadcrumbs = 5; - - final List items; - final void Function(InspectorTreeNode?) onTap; - - @override - Widget build(BuildContext context) { - if (items.isEmpty) { - return const SizedBox(); - } - - final breadcrumbs = _generateBreadcrumbs(items); - return SizedBox( - height: Breadcrumb.height, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Row( - children: breadcrumbs.map((item) { - if (item.isChevron) { - return const Icon(Icons.chevron_right, size: defaultIconSize); - } - - return Flexible( - child: _InspectorBreadcrumb( - data: item, - onTap: () => onTap(item.node), - ), - ); - }).toList(), - ), - ), - ); - } - - List<_InspectorBreadcrumbData> _generateBreadcrumbs( - List nodes, - ) { - final lastNode = nodes.safeLast; - final items = nodes.map((node) { - return _InspectorBreadcrumbData.wrap( - node: node, - isSelected: node == lastNode, - ); - }).toList(); - List<_InspectorBreadcrumbData> breadcrumbs; - breadcrumbs = items.length > _maxNumberOfBreadcrumbs - ? [ - items[0], - _InspectorBreadcrumbData.more(), - ...items.sublist( - items.length - _maxNumberOfBreadcrumbs + 1, - items.length, - ), - ] - : items; - - return breadcrumbs.joinWith(_InspectorBreadcrumbData.chevron()); - } -} - -class _InspectorBreadcrumb extends StatelessWidget { - const _InspectorBreadcrumb({required this.data, required this.onTap}); - - static const _iconScale = 0.75; - - final _InspectorBreadcrumbData data; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final text = Text( - data.text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: DiagnosticsTextStyles.regular( - Theme.of(context).colorScheme, - ).copyWith(fontSize: 11), - ); - - final icon = data.icon == null - ? null - : Transform.scale( - scale: _iconScale, - child: Padding( - padding: const EdgeInsets.only(right: iconPadding), - child: data.icon, - ), - ); - - return InkWell( - onTap: data.isClickable ? onTap : null, - borderRadius: defaultBorderRadius, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: densePadding, - vertical: borderPadding, - ), - decoration: BoxDecoration( - borderRadius: defaultBorderRadius, - color: data.isSelected - ? Theme.of(context).colorScheme.selectedRowBackgroundColor - : Colors.transparent, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) icon, - Flexible(child: text), - ], - ), - ), - ); - } -} - -class _InspectorBreadcrumbData { - const _InspectorBreadcrumbData._({ - required this.node, - required this.isSelected, - required this.alternativeText, - required this.alternativeIcon, - }); - - factory _InspectorBreadcrumbData.wrap({ - required InspectorTreeNode node, - required bool isSelected, - }) { - return _InspectorBreadcrumbData._( - node: node, - isSelected: isSelected, - alternativeText: null, - alternativeIcon: null, - ); - } - - /// Construct a special item for showing '…' symbol between other items - factory _InspectorBreadcrumbData.more() { - return const _InspectorBreadcrumbData._( - node: null, - isSelected: false, - alternativeText: _ellipsisValue, - alternativeIcon: null, - ); - } - - factory _InspectorBreadcrumbData.chevron() { - return const _InspectorBreadcrumbData._( - node: null, - isSelected: false, - alternativeText: null, - alternativeIcon: _breadcrumbSeparatorIcon, - ); - } - - static const _ellipsisValue = '…'; - static const _breadcrumbSeparatorIcon = Icons.chevron_right; - - final InspectorTreeNode? node; - final IconData? alternativeIcon; - final String? alternativeText; - final bool isSelected; - - String get text => alternativeText ?? node?.diagnostic?.description ?? ''; - - Widget? get icon { - if (alternativeIcon != null) { - return const Icon(_breadcrumbSeparatorIcon, size: defaultIconSize); - } - - return node?.diagnostic?.icon; - } - - bool get isChevron => - node == null && alternativeIcon == _breadcrumbSeparatorIcon; - - bool get isEllipsis => node == null && alternativeText == _ellipsisValue; - - bool get isClickable => !isSelected && !isEllipsis; -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart deleted file mode 100644 index 9ddc8196e90..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart +++ /dev/null @@ -1,950 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -/// This library must not have direct dependencies on dart:html. -/// -/// This allows tests of the complicated logic in this class to run on the VM -/// and will help simplify porting this code to work with Hummingbird. -/// -/// This code is directly based on -/// src/io/flutter/view/InspectorPanel.java -/// with some refactors to make the code more of a controller than a combination -/// of view and controller. View specific portions of InspectorPanel.java have -/// been moved to inspector.dart. -library; - -import 'dart:async'; - -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:vm_service/vm_service.dart'; - -import '../../service/service_extensions.dart' as extensions; -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree.dart'; -import '../../shared/console/primitives/simple_items.dart'; -import '../../shared/diagnostics/diagnostics_node.dart'; -import '../../shared/diagnostics/inspector_service.dart'; -import '../../shared/diagnostics/primitives/instance_ref.dart'; -import '../../shared/globals.dart'; -import '../../shared/primitives/query_parameters.dart'; -import '../../shared/primitives/utils.dart'; -import '../../shared/utils/utils.dart'; -import '../inspector_shared/inspector_screen.dart'; -import 'inspector_tree_controller.dart'; - -final _log = Logger('inspector_controller'); - -/// This class is based on the InspectorPanel class from the Flutter IntelliJ -/// plugin with some refactors to make it more of a true controller than a view. -class InspectorController extends DisposableController - with AutoDisposeControllerMixin - implements InspectorServiceClient { - InspectorController({ - required this.inspectorTree, - InspectorTreeController? detailsTree, - required this.treeType, - this.parent, - this.isSummaryTree = true, - }) : assert((detailsTree != null) == isSummaryTree) { - unawaited(_init(detailsTree: detailsTree)); - } - - Future _init({InspectorTreeController? detailsTree}) async { - _refreshRateLimiter = RateLimiter(refreshFramesPerSecond, refresh); - - inspectorTree.config = InspectorTreeConfig( - onNodeAdded: _onNodeAdded, - onSelectionChange: selectionChanged, - onExpand: _onExpand, - onClientActiveChange: _onClientChange, - ); - details = isSummaryTree - ? InspectorController( - inspectorTree: detailsTree!, - treeType: treeType, - parent: this, - isSummaryTree: false, - ) - : null; - - await serviceConnection.serviceManager.onServiceAvailable; - - if (inspectorService is InspectorService) { - _treeGroups = InspectorObjectGroupManager( - serviceConnection.inspectorService as InspectorService, - 'tree', - ); - _selectionGroups = InspectorObjectGroupManager( - serviceConnection.inspectorService as InspectorService, - 'selection', - ); - } - - addAutoDisposeListener( - serviceConnection.serviceManager.isolateManager.mainIsolate, - () { - final isolate = - serviceConnection.serviceManager.isolateManager.mainIsolate.value; - if (isolate != _mainIsolate) { - onIsolateStopped(); - } - _mainIsolate = isolate; - }, - ); - - // This logic only needs to be run once so run it in the outermost - // controller. - if (parent == null) { - // If select mode is available, enable the on device inspector as it - // won't interfere with users. - addAutoDisposeListener(_supportsToggleSelectWidgetMode, () { - if (_supportsToggleSelectWidgetMode.value) { - safeUnawaited( - serviceConnection.serviceManager.serviceExtensionManager - .setServiceExtensionState( - extensions.enableOnDeviceInspector.extension, - enabled: true, - value: true, - ), - ); - } - }); - } - - addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () { - if (serviceConnection.serviceManager.connectedState.value.connected) { - _handleConnectionStart(); - } else { - _handleConnectionStop(); - } - }); - - if (serviceConnection.serviceManager.connectedAppInitialized) { - _handleConnectionStart(); - } - - serviceConnection.consoleService.ensureServiceInitialized(); - } - - void _handleConnectionStart() { - // Clear any existing badge/errors for older errors that were collected. - // Do this in a post frame callback so that we are not trying to clear the - // error notifiers for this screen while the framework is already in the - // process of building widgets. - // TODO(kenz): When this method is called outside createState(), this post - // frame callback can be removed. - WidgetsBinding.instance.addPostFrameCallback((_) { - serviceConnection.errorBadgeManager.clearErrorCount(InspectorScreen.id); - }); - filterErrors(); - } - - void _handleConnectionStop() { - setActivate(false); - if (isSummaryTree) { - dispose(); - } - } - - IsolateRef? _mainIsolate; - - ValueListenable get _supportsToggleSelectWidgetMode => serviceConnection - .serviceManager - .serviceExtensionManager - .hasServiceExtension(extensions.toggleSelectWidgetMode.extension); - - void _onClientChange(bool added) { - if (!added && _clientCount == 0) { - // Don't try to remove clients if there are none - return; - } - - _clientCount += added ? 1 : -1; - assert(_clientCount >= 0); - if (_clientCount == 1) { - setVisibleToUser(true); - setActivate(true); - } else if (_clientCount == 0) { - setVisibleToUser(false); - } - } - - int _clientCount = 0; - - /// Maximum frame rate to refresh the inspector at to avoid taxing the - /// physical device with too many requests to recompute properties and trees. - /// - /// A value up to around 30 frames per second could be reasonable for - /// debugging highly interactive cases particularly when the user is on a - /// simulator or high powered native device. The frame rate is set low - /// for now mainly to minimize risk. - static const refreshFramesPerSecond = 5.0; - - final bool isSummaryTree; - - /// Parent InspectorController if this is a details subtree. - InspectorController? parent; - - InspectorController? details; - - InspectorTreeController inspectorTree; - final FlutterTreeType treeType; - - late RateLimiter _refreshRateLimiter; - - InspectorServiceBase get inspectorService => - serviceConnection.inspectorService as InspectorServiceBase; - - /// Groups used to manage and cancel requests to load data to display directly - /// in the tree. - InspectorObjectGroupManager? _treeGroups; - - /// Groups used to manage and cancel requests to determine what the current - /// selection is. - /// - /// This group needs to be kept separate from treeGroups as the selection is - /// shared more with the details subtree. - /// TODO(jacobr): is there a way we can unify the selection and tree groups? - InspectorObjectGroupManager? _selectionGroups; - - /// Node being highlighted due to the current hover. - InspectorTreeNode? get currentShowNode => inspectorTree.hover; - - set currentShowNode(InspectorTreeNode? node) => inspectorTree.hover = node; - - bool flutterAppFrameReady = false; - - bool treeLoadStarted = false; - - RemoteDiagnosticsNode? subtreeRoot; - - bool programmaticSelectionChangeInProgress = false; - - ValueListenable get selectedNode => _selectedNode; - final _selectedNode = ValueNotifier(null); - - InspectorTreeNode? lastExpanded; - - bool isActive = false; - - final valueToInspectorTreeNode = {}; - - /// When visibleToUser is false we should dispose all allocated objects and - /// not perform any actions. - bool visibleToUser = false; - - bool highlightNodesShownInBothTrees = false; - - bool get detailsSubtree => parent != null; - - RemoteDiagnosticsNode? get selectedDiagnostic => - selectedNode.value?.diagnostic; - - ValueListenable get selectedErrorIndex => _selectedErrorIndex; - final _selectedErrorIndex = ValueNotifier(null); - - /// Tracks whether the first load of the inspector tree has been completed. - /// - /// This field is used to prevent sending multiple analytics events for - /// inspector tree load timing. - bool firstInspectorTreeLoadCompleted = false; - - FlutterTreeType getTreeType() { - return treeType; - } - - void setVisibleToUser(bool visible) { - if (visibleToUser == visible) { - return; - } - visibleToUser = visible; - - if (visibleToUser) { - if (parent == null) { - unawaited(maybeLoadUI()); - } - } else { - shutdownTree(false); - } - } - - bool hasDiagnosticsValue(InspectorInstanceRef ref) { - return valueToInspectorTreeNode.containsKey(ref); - } - - RemoteDiagnosticsNode? findDiagnosticsValue(InspectorInstanceRef ref) { - return valueToInspectorTreeNode[ref]?.diagnostic; - } - - void endShowNode() { - highlightShowNode(null); - } - - bool highlightShowFromNodeInstanceRef(InspectorInstanceRef ref) { - return highlightShowNode(valueToInspectorTreeNode[ref]); - } - - bool highlightShowNode(InspectorTreeNode? node) { - if (node == null && parent != null) { - // If nothing is highlighted, highlight the node selected in the parent - // tree so user has context of where the node selected in the parent is - // in the details tree. - node = findMatchingInspectorTreeNode(parent?.selectedDiagnostic); - } - - currentShowNode = node; - return true; - } - - InspectorTreeNode? findMatchingInspectorTreeNode( - RemoteDiagnosticsNode? node, - ) { - final valueRef = node?.valueRef; - if (valueRef == null) { - return null; - } - return valueToInspectorTreeNode[valueRef]; - } - - Future _waitForPendingUpdateDone() async { - // Wait for the selection to be resolved followed by waiting for the tree to be computed. - await _selectionGroups?.pendingUpdateDone; - await _treeGroups?.pendingUpdateDone; - // TODO(jacobr): are there race conditions we need to think more carefully about here? - } - - Future refresh() { - if (!visibleToUser) { - // We will refresh again once we are visible. - // There is a risk a refresh got triggered before the view was visble. - return Future.value(); - } - - // TODO(jacobr): refresh the tree as well as just the properties. - final detailsLocal = details; - if (detailsLocal == null) return _waitForPendingUpdateDone(); - - return [ - _waitForPendingUpdateDone(), - detailsLocal._waitForPendingUpdateDone(), - ].wait; - } - - // Note that this may be called after the controller is disposed. We need to handle nulls in the fields. - void shutdownTree(bool isolateStopped) { - // It is critical we clear all data that is kept alive by inspector object - // references in this method as that stale data will trigger inspector - // exceptions. - programmaticSelectionChangeInProgress = true; - _treeGroups?.clear(isolateStopped); - _selectionGroups?.clear(isolateStopped); - - currentShowNode = null; - _selectedNode.value = null; - lastExpanded = null; - - subtreeRoot = null; - - inspectorTree.root = inspectorTree.createNode(); - programmaticSelectionChangeInProgress = false; - valueToInspectorTreeNode.clear(); - } - - void onIsolateStopped() { - flutterAppFrameReady = false; - treeLoadStarted = false; - shutdownTree(true); - } - - @override - Future onForceRefresh() async { - assert(!disposed); - if (!visibleToUser || disposed) { - return; - } - await _recomputeTreeRoot(null, null, false); - if (disposed) { - return; - } - - filterErrors(); - - return _waitForPendingUpdateDone(); - } - - void filterErrors() { - if (isSummaryTree) { - serviceConnection.errorBadgeManager.filterErrors( - InspectorScreen.id, - (id) => hasDiagnosticsValue(InspectorInstanceRef(id)), - ); - } - } - - void setActivate(bool enabled) { - if (!enabled) { - onIsolateStopped(); - isActive = false; - return; - } - if (isActive) { - // Already activated. - return; - } - - isActive = true; - inspectorService.addClient(this); - unawaited(maybeLoadUI()); - } - - Future maybeLoadUI() async { - if (parent != null) { - // The parent controller will drive loading the UI. - return; - } - if (!visibleToUser || !isActive) { - return; - } - - if (flutterAppFrameReady) { - if (disposed) return; - // We need to start by querying the inspector service to find out the - // current state of the UI. - final inspectorRef = DevToolsQueryParams.load().inspectorRef; - await updateSelectionFromService( - firstFrame: true, - inspectorRef: inspectorRef, - ); - } else { - if (disposed) return; - if (inspectorService is InspectorService) { - final widgetTreeReady = await (inspectorService as InspectorService) - .isWidgetTreeReady(); - flutterAppFrameReady = widgetTreeReady; - } - if (isActive && flutterAppFrameReady) { - await maybeLoadUI(); - } - } - } - - Future _recomputeTreeRoot( - RemoteDiagnosticsNode? newSelection, - RemoteDiagnosticsNode? detailsSelection, - bool setSubtreeRoot, { - int subtreeDepth = 2, - }) async { - assert(!disposed); - final treeGroups = _treeGroups; - if (disposed || treeGroups == null) { - return; - } - - treeGroups.cancelNext(); - try { - final group = treeGroups.next; - final node = await (detailsSubtree - ? group.getDetailsSubtree(subtreeRoot, subtreeDepth: subtreeDepth) - : group.getRoot(treeType, isSummaryTree: true)); - if (node == null || group.disposed || disposed) { - return; - } - // TODO(jacobr): as a performance optimization we should check if the - // new tree is identical to the existing tree in which case we should - // dispose the new tree and keep the old tree. - treeGroups.promoteNext(); - _clearValueToInspectorTreeNodeMapping(); - - final rootNode = inspectorTree.setupInspectorTreeNode( - inspectorTree.createNode(), - node, - expandChildren: true, - expandProperties: false, - ); - inspectorTree.root = rootNode; - - refreshSelection(newSelection, detailsSelection, setSubtreeRoot); - } catch (error, st) { - _log.shout(error, error, st); - treeGroups.cancelNext(); - return; - } - } - - void _clearValueToInspectorTreeNodeMapping() { - valueToInspectorTreeNode.clear(); - } - - /// Show the details subtree starting with node subtreeRoot highlighting - /// node subtreeSelection. - void _showDetailSubtrees( - RemoteDiagnosticsNode? subtreeRoot, - RemoteDiagnosticsNode? subtreeSelection, - ) { - this.subtreeRoot = subtreeRoot; - details?.setSubtreeRoot(subtreeRoot, subtreeSelection); - } - - void setSubtreeRoot( - RemoteDiagnosticsNode? node, - RemoteDiagnosticsNode? selection, - ) { - assert(detailsSubtree); - selection ??= node; - if (node != null && node == subtreeRoot) { - // Select the new node in the existing subtree. - applyNewSelection(selection, null, false); - return; - } - subtreeRoot = node; - if (node == null) { - // Passing in a null node indicates we should clear the subtree and free any memory allocated. - shutdownTree(false); - return; - } - - // Clear now to eliminate frame of highlighted nodes flicker. - _clearValueToInspectorTreeNodeMapping(); - unawaited(_recomputeTreeRoot(selection, null, false)); - } - - InspectorTreeNode? getSubtreeRootNode() { - if (subtreeRoot == null) { - return null; - } - return valueToInspectorTreeNode[subtreeRoot!.valueRef]; - } - - void refreshSelection( - RemoteDiagnosticsNode? newSelection, - RemoteDiagnosticsNode? detailsSelection, - bool setSubtreeRoot, - ) { - newSelection ??= selectedDiagnostic; - setSelectedNode(findMatchingInspectorTreeNode(newSelection)); - syncSelectionHelper( - maybeRerootDetailsTree: setSubtreeRoot, - selection: newSelection, - detailsSelection: detailsSelection, - ); - - final detailsLocal = details; - if (detailsLocal != null) { - if (subtreeRoot != null && getSubtreeRootNode() == null) { - subtreeRoot = newSelection; - detailsLocal.setSubtreeRoot(newSelection, detailsSelection); - } - } - syncTreeSelection(); - } - - void syncTreeSelection() { - programmaticSelectionChangeInProgress = true; - inspectorTree.selection = selectedNode.value; - inspectorTree.expandPath(selectedNode.value); - programmaticSelectionChangeInProgress = false; - animateTo(selectedNode.value); - } - - void selectAndShowNode(RemoteDiagnosticsNode? node) { - if (node == null) { - return; - } - selectAndShowInspectorInstanceRef(node.valueRef); - } - - void selectAndShowInspectorInstanceRef(InspectorInstanceRef ref) { - final node = valueToInspectorTreeNode[ref]; - if (node == null) { - return; - } - setSelectedNode(node); - syncTreeSelection(); - } - - InspectorTreeNode? getTreeNode(RemoteDiagnosticsNode node) { - return valueToInspectorTreeNode[node.valueRef]; - } - - void maybeUpdateValueUI(InspectorInstanceRef valueRef) { - final node = valueToInspectorTreeNode[valueRef]; - if (node == null) { - // The value isn't shown in the parent tree. Nothing to do. - return; - } - inspectorTree.nodeChanged(node); - } - - @override - void onFlutterFrame() { - flutterAppFrameReady = true; - if (!visibleToUser) { - return; - } - - if (!treeLoadStarted) { - treeLoadStarted = true; - // This was the first frame. - unawaited(maybeLoadUI()); - } - _refreshRateLimiter.scheduleRequest(); - } - - @override - void onInspectorSelectionChanged() { - if (!visibleToUser) { - // Don't do anything. We will update the view once it is visible again. - return; - } - if (detailsSubtree) { - // Wait for the master to update. - return; - } - unawaited(updateSelectionFromService(firstFrame: false)); - } - - Future updateSelectionFromService({ - required bool firstFrame, - String? inspectorRef, - }) async { - if (parent != null) { - // If we have a parent controller we should wait for the parent to update - // our selection rather than updating it our self. - return; - } - final selectionGroups = _selectionGroups; - if (selectionGroups == null) { - // Already disposed. Ignore this requested to update selection. - return; - } - treeLoadStarted = true; - - selectionGroups.cancelNext(); - - final group = selectionGroups.next; - - if (inspectorRef != null) { - await group.setSelectionInspector( - InspectorInstanceRef(inspectorRef), - false, - ); - if (disposed) return; - } - final pendingSelectionFuture = group.getSelection( - selectedDiagnostic, - treeType, - restrictToLocalProject: isSummaryTree, - ); - - final pendingDetailsFuture = isSummaryTree - ? group.getSelection(selectedDiagnostic, treeType) - : null; - - try { - final newSelection = await pendingSelectionFuture; - if (disposed || group.disposed) return; - RemoteDiagnosticsNode? detailsSelection; - - if (pendingDetailsFuture != null) { - detailsSelection = await pendingDetailsFuture; - if (disposed || group.disposed) return; - } - - if (!firstFrame && - detailsSelection?.valueRef == details?.selectedDiagnostic?.valueRef && - newSelection?.valueRef == selectedDiagnostic?.valueRef) { - // No need to change the selection as it didn't actually change. - selectionGroups.cancelNext(); - return; - } - selectionGroups.promoteNext(); - - subtreeRoot = newSelection; - - applyNewSelection(newSelection, detailsSelection, true); - - // Send an event that a widget was selected on the device. - ga.select( - gac.inspector, - gac.onDeviceSelection, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), - ); - } catch (error, st) { - if (selectionGroups.next == group) { - _log.shout(error, error, st); - selectionGroups.cancelNext(); - } - } - } - - void applyNewSelection( - RemoteDiagnosticsNode? newSelection, - RemoteDiagnosticsNode? detailsSelection, - bool setSubtreeRoot, - ) { - final nodeInTree = findMatchingInspectorTreeNode(newSelection); - - if (nodeInTree == null) { - // The tree has probably changed since we last updated. Do a full refresh - // so that the tree includes the new node we care about. - unawaited( - _recomputeTreeRoot(newSelection, detailsSelection, setSubtreeRoot), - ); - } - - refreshSelection(newSelection, detailsSelection, setSubtreeRoot); - } - - void animateTo(InspectorTreeNode? node) { - if (node == null) { - return; - } - - inspectorTree.animateToTargets([node]); - } - - void setSelectedNode(InspectorTreeNode? newSelection) { - if (newSelection == selectedNode.value) { - return; - } - - _selectedNode.value = newSelection; - - lastExpanded = null; // New selected node takes precedence. - endShowNode(); - final detailsLocal = details; - final parantLocal = parent; - if (detailsLocal != null) { - detailsLocal.endShowNode(); - } else if (parantLocal != null) { - parantLocal.endShowNode(); - } - - _updateSelectedErrorFromNode(_selectedNode.value); - animateTo(selectedNode.value); - } - - /// Update the index of the selected error based on a node that has been - /// selected in the tree. - void _updateSelectedErrorFromNode(InspectorTreeNode? node) { - final inspectorRef = node?.diagnostic?.valueRef.id; - - final errors = serviceConnection.errorBadgeManager - .erroredItemsForPage(InspectorScreen.id) - .value; - - // Check whether the node that was just selected has any errors associated - // with it. - var errorIndex = inspectorRef != null - ? errors.keys.toList().indexOf(inspectorRef) - : null; - if (errorIndex == -1) { - errorIndex = null; - } - - _selectedErrorIndex.value = errorIndex; - - if (errorIndex != null) { - // Mark the error as "seen" as this will render slightly differently - // so the user can track which errored nodes they've viewed. - serviceConnection.errorBadgeManager.markErrorAsRead( - InspectorScreen.id, - errors[inspectorRef!]!, - ); - // Also clear the error badge since new errors may have arrived while - // the inspector was visible (normally they're cleared when visiting - // the screen) and visiting an errored node seems an appropriate - // acknowledgement of the errors. - serviceConnection.errorBadgeManager.clearErrorCount(InspectorScreen.id); - } - } - - /// Updates the index of the selected error and selects its node in the tree. - void selectErrorByIndex(int index) { - _selectedErrorIndex.value = index; - - final errors = serviceConnection.errorBadgeManager - .erroredItemsForPage(InspectorScreen.id) - .value; - - unawaited( - updateSelectionFromService( - firstFrame: false, - inspectorRef: errors.keys.elementAt(index), - ), - ); - } - - void _onExpand(InspectorTreeNode node) { - unawaited(inspectorTree.maybePopulateChildren(node)); - } - - Future _addNodeToConsole(InspectorTreeNode node) async { - final valueRef = node.diagnostic!.valueRef; - final isolateRef = inspectorService.isolateRef; - final instanceRef = await node.diagnostic!.objectGroupApi - ?.toObservatoryInstanceRef(valueRef); - if (disposed) return; - - if (instanceRef != null) { - await serviceConnection.consoleService.appendInstanceRef( - value: instanceRef, - diagnostic: node.diagnostic, - isolateRef: isolateRef, - forceScrollIntoView: true, - ); - } - } - - void selectionChanged() { - if (!visibleToUser) { - return; - } - - final node = inspectorTree.selection; - if (node != null) { - unawaited(inspectorTree.maybePopulateChildren(node)); - } - if (programmaticSelectionChangeInProgress) { - return; - } - if (node != null) { - setSelectedNode(node); - unawaited(_addNodeToConsole(node)); - - // Don't reroot if the selected value is already visible in the details tree. - final maybeReroot = - isSummaryTree && - details != null && - selectedDiagnostic != null && - !details!.hasDiagnosticsValue(selectedDiagnostic!.valueRef); - syncSelectionHelper( - maybeRerootDetailsTree: maybeReroot, - selection: selectedDiagnostic, - detailsSelection: selectedDiagnostic, - ); - - if (!maybeReroot) { - final parantLocal = parent; - final detailsLocal = details; - - if (isSummaryTree && detailsLocal != null) { - detailsLocal.selectAndShowNode(selectedDiagnostic); - } else if (parantLocal != null) { - parantLocal.selectAndShowNode( - firstAncestorInParentTree(selectedNode.value), - ); - } - } - } - } - - RemoteDiagnosticsNode? firstAncestorInParentTree(InspectorTreeNode? node) { - final parentLocal = parent; - - if (parentLocal == null) { - return node?.diagnostic; - } - while (node != null) { - final diagnostic = node.diagnostic; - if (diagnostic != null && - parentLocal.hasDiagnosticsValue(diagnostic.valueRef)) { - return parentLocal.findDiagnosticsValue(diagnostic.valueRef); - } - node = node.parent; - } - return null; - } - - void syncSelectionHelper({ - required bool maybeRerootDetailsTree, - required RemoteDiagnosticsNode? selection, - required RemoteDiagnosticsNode? detailsSelection, - }) { - if (selection != null) { - if (selection.isCreatedByLocalProject) { - _navigateTo(selection); - } - } - if (detailsSubtree || details == null) { - if (selection != null) { - var toSelect = selectedNode.value; - - while (toSelect != null && toSelect.diagnostic!.isProperty) { - toSelect = toSelect.parent; - } - - if (toSelect != null) { - final diagnosticToSelect = toSelect.diagnostic!; - unawaited(diagnosticToSelect.setSelectionInspector(true)); - } - } - } - - if (maybeRerootDetailsTree) { - _showDetailSubtrees(selection, detailsSelection); - } else if (selection != null) { - // We can't rely on the details tree to update the selection on the server in this case. - unawaited(selection.setSelectionInspector(true)); - } - } - - // TODO(jacobr): implement this method and use the parameter. - // ignore: avoid-unused-parameters - void _navigateTo(RemoteDiagnosticsNode diagnostic) { - // TODO(jacobr): dispatch an event over the inspectorService requesting a - // navigate operation. - } - - @override - void dispose() { - assert(!disposed); - if (serviceConnection.inspectorService != null) { - shutdownTree(false); - } - _treeGroups?.clear(false); - _treeGroups = null; - _selectionGroups?.clear(false); - _selectionGroups = null; - details?.dispose(); - - _refreshRateLimiter.dispose(); - _selectedNode.dispose(); - _selectedErrorIndex.dispose(); - super.dispose(); - } - - void _onNodeAdded( - InspectorTreeNode node, - RemoteDiagnosticsNode diagnosticsNode, - ) { - final valueRef = diagnosticsNode.valueRef; - // Properties do not have unique values so should not go in the valueToInspectorTreeNode map. - if (valueRef.id != null && !diagnosticsNode.isProperty) { - valueToInspectorTreeNode[valueRef] = node; - } - } - - Future expandAllNodesInDetailsTree() async { - final detailsLocal = details!; - await detailsLocal._recomputeTreeRoot( - inspectorTree.selection?.diagnostic, - detailsLocal.inspectorTree.selection?.diagnostic ?? - detailsLocal.inspectorTree.root?.diagnostic, - false, - subtreeDepth: maxJsInt, - ); - } - - void collapseDetailsToSelected() { - final detailsLocal = details!; - detailsLocal.inspectorTree.collapseToSelected(); - detailsLocal.animateTo(detailsLocal.inspectorTree.selection); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart deleted file mode 100644 index a12ed91f76e..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_data_models.dart +++ /dev/null @@ -1,887 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -/// @docImport 'layout_explorer/ui/overflow_indicator_painter.dart'; -library; - -import 'dart:math' as math; - -import 'package:flutter/rendering.dart'; - -import '../../shared/diagnostics/diagnostics_node.dart'; -import '../../shared/primitives/math_utils.dart'; -import '../../shared/primitives/utils.dart'; -import 'layout_explorer/flex/utils.dart'; - -const overflowEpsilon = 0.1; - -/// Compute real widget sizes into rendered sizes to be displayed on the details tab. -/// The sum of the resulting render sizes may or may not be greater than the [maxSizeAvailable] -/// In the case where it is greater, we should render it with scrolling capability. -/// -/// Variables: -/// - [sizes] : real size for widgets that want to be rendered / scaled -/// - [smallestSize] : the smallest element in the array [sizes] -/// - [largestSize] : the largest element in the array [sizes] -/// - [smallestRenderSize] : render size for smallest element -/// - [largestRenderSize] : render size for largest element -/// - [maxSizeAvailable] : maximum size available for rendering the widget -/// - [useMaxSizeAvailable] : flag for forcing the widget dimension to be at least [maxSizeAvailable] -/// -/// if [useMaxSizeAvailable] is set to true, -/// this method will ignore the largestRenderSize -/// and compute its own largestRenderSize to force -/// the sum of the render size to be equals to [maxSizeAvailable] -/// -/// Formula for computing render size: -/// ``` -/// renderSize[i] = (size[i] - smallestSize) -/// * (largestRenderSize - smallestRenderSize) -/// / (largestSize - smallestSize) + smallestRenderSize -/// ``` -/// Explanation: -/// - The computation formula for transforming size to renderSize is based on these two things: -/// - smallest element will be rendered to [smallestRenderSize] -/// - largest element will be rendered to [largestRenderSize] -/// - any other size will be scaled accordingly -/// - The formula above is derived from: -/// ``` -/// (renderSize[i] - smallestRenderSize) / (largestRenderSize - smallestRenderSize) -/// = (size[i] - smallestSize) / (size[i] - smallestSize) -/// ``` -/// -/// Formula for computing forced [largestRenderSize]: -/// ``` -/// largestRenderSize = (maxSizeAvailable - sizes.length * smallestRenderSize) -/// * (largestSize - smallestSize) / sum(s[i] - ss) + smallestRenderSize -/// ``` -/// Explanation: -/// - This formula is derived from the equation: -/// ``` -/// sum(renderSize) = maxSizeAvailable -/// ``` -List computeRenderSizes({ - required Iterable sizes, - required double smallestSize, - required double largestSize, - required double smallestRenderSize, - required double largestRenderSize, - required double maxSizeAvailable, - bool useMaxSizeAvailable = true, -}) { - final n = sizes.length; - - if (smallestSize == largestSize) { - // It means that all widget have the same size - // and we can just divide the size evenly - // but it should be at least as big as [smallestRenderSize] - final renderSize = math.max(smallestRenderSize, maxSizeAvailable / n); - return [for (final _ in sizes) renderSize]; - } - - List transformToRenderSize(double largestRenderSize) => [ - for (final s in sizes) - (s - smallestSize) * - (largestRenderSize - smallestRenderSize) / - (largestSize - smallestSize) + - smallestRenderSize, - ]; - - var renderSizes = transformToRenderSize(largestRenderSize); - - if (useMaxSizeAvailable && sum(renderSizes) < maxSizeAvailable) { - largestRenderSize = - (maxSizeAvailable - n * smallestRenderSize) * - (largestSize - smallestSize) / - sum([for (final s in sizes) s - smallestSize]) + - smallestRenderSize; - renderSizes = transformToRenderSize(largestRenderSize); - } - return renderSizes; -} - -// TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed -/// Represents parsed layout information for a specific [RemoteDiagnosticsNode]. -class LayoutProperties { - LayoutProperties(this.node, {int copyLevel = 1}) - : description = node.description, - size = node.size!, - constraints = node.constraints, - isFlex = node.isFlex, - flexFactor = node.flexFactor, - flexFit = node.flexFit, - children = copyLevel == 0 - ? [] - : node.childrenNow - .map( - (child) => LayoutProperties(child, copyLevel: copyLevel - 1), - ) - .toList(growable: false) { - for (final child in children) { - child.parent = this; - } - } - - LayoutProperties.values({ - required this.node, - required this.children, - required this.constraints, - required this.description, - required this.flexFactor, - required this.isFlex, - required this.size, - required this.flexFit, - }) { - for (final child in children) { - child.parent = this; - } - } - - LayoutProperties? parent; - final RemoteDiagnosticsNode node; - final List children; - final BoxConstraints? constraints; - final String? description; - final num? flexFactor; - final FlexFit? flexFit; - final bool isFlex; - final Size size; - - /// Represents the order of [children] to be displayed. - List get displayChildren => children; - - bool get hasFlexFactor { - final flexFactorLocal = flexFactor; - if (flexFactorLocal == null) return false; - return flexFactorLocal > 0; - } - - int get totalChildren => children.length; - - bool get hasChildren => children.isNotEmpty; - - double get width => size.width; - - double get height => size.height; - - double dimension(Axis axis) => axis == Axis.horizontal ? width : height; - - List childrenDimensions(Axis axis) { - return displayChildren.map((child) => child.dimension(axis)).toList(); - } - - List get childrenWidths => childrenDimensions(Axis.horizontal); - - List get childrenHeights => childrenDimensions(Axis.vertical); - - String describeWidthConstraints() { - final constraintsLocal = constraints; - if (constraintsLocal == null) return ''; - return constraintsLocal.hasBoundedWidth - ? describeAxis( - constraintsLocal.minWidth, - constraintsLocal.maxWidth, - 'w', - ) - : 'width is unconstrained'; - } - - String describeHeightConstraints() { - final constraintsLocal = constraints; - if (constraintsLocal == null) return ''; - return constraintsLocal.hasBoundedHeight - ? describeAxis( - constraintsLocal.minHeight, - constraintsLocal.maxHeight, - 'h', - ) - : 'height is unconstrained'; - } - - String describeWidth() => 'w=${toStringAsFixed(size.width)}'; - - String describeHeight() => 'h=${toStringAsFixed(size.height)}'; - - bool get isOverflowWidth { - final parentWidth = parent?.width; - if (parentWidth == null) return false; - final parentData = node.parentData; - double widthUsed = width; - - widthUsed += parentData.offset.dx; - - // TODO(jacobr): certain widgets may allow overflow so this may false - // positive a bit for cases like Stack. - return widthUsed > parentWidth + overflowEpsilon; - } - - bool get isOverflowHeight { - final parentHeight = parent?.height; - if (parentHeight == null) return false; - final parentData = node.parentData; - double heightUsed = height; - - heightUsed += parentData.offset.dy; - - return heightUsed > parentHeight + overflowEpsilon; - } - - static String describeAxis(double min, double max, String axis) { - if (min == max) return '$axis=${min.toStringAsFixed(1)}'; - return '${min.toStringAsFixed(1)}<=$axis<=${max.toStringAsFixed(1)}'; - } - - LayoutProperties copyWith({ - List? children, - BoxConstraints? constraints, - String? description, - int? flexFactor, - FlexFit? flexFit, - bool? isFlex, - Size? size, - }) { - return LayoutProperties.values( - node: node, - children: children ?? this.children, - constraints: constraints ?? this.constraints, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - isFlex: isFlex ?? this.isFlex, - size: size ?? this.size, - flexFit: flexFit ?? this.flexFit, - ); - } -} - -/// Enum object to represent which side of the widget is overflowing. -/// -/// See also: -/// * [OverflowIndicatorPainter] -enum OverflowSide { right, bottom } - -// TODO(jacobr): is it possible to overflow on multiple sides? -// TODO(jacobr): do we need to worry about overflowing on the left side in RTL -// layouts? We need to audit the Flutter semantics for determining overflow to -// make sure we are consistent. -extension LayoutPropertiesExtension on LayoutProperties { - OverflowSide? get overflowSide { - if (isOverflowWidth) return OverflowSide.right; - if (isOverflowHeight) return OverflowSide.bottom; - return null; - } -} - -final _flexLayoutExpando = Expando(); - -extension MainAxisAlignmentExtension on MainAxisAlignment { - MainAxisAlignment get reversed { - switch (this) { - case MainAxisAlignment.start: - return MainAxisAlignment.end; - case MainAxisAlignment.end: - return MainAxisAlignment.start; - default: - return this; - } - } -} - -/// TODO(albertusangga): Move this to [RemoteDiagnosticsNode] once dart:html app is removed. -class FlexLayoutProperties extends LayoutProperties { - FlexLayoutProperties({ - required super.size, - required super.children, - required super.node, - super.constraints, - super.isFlex = false, - super.description, - super.flexFactor, - super.flexFit, - this.direction = Axis.vertical, - this.mainAxisAlignment, - this.crossAxisAlignment, - this.mainAxisSize, - required this.textDirection, - required this.verticalDirection, - this.textBaseline, - }) : super.values(); - - FlexLayoutProperties._fromNode( - super.node, { - this.direction = Axis.vertical, - this.mainAxisAlignment, - this.mainAxisSize, - this.crossAxisAlignment, - required this.textDirection, - required this.verticalDirection, - this.textBaseline, - }); - - factory FlexLayoutProperties.fromDiagnostics(RemoteDiagnosticsNode node) { - // Cache the properties on an expando so that local tweaks to - // FlexLayoutProperties persist across multiple lookups from an - // RemoteDiagnosticsNode. - return _flexLayoutExpando[node] ??= _buildNode(node); - } - - @override - FlexLayoutProperties copyWith({ - Size? size, - List? children, - BoxConstraints? constraints, - bool? isFlex, - String? description, - num? flexFactor, - FlexFit? flexFit, - Axis? direction, - MainAxisAlignment? mainAxisAlignment, - MainAxisSize? mainAxisSize, - CrossAxisAlignment? crossAxisAlignment, - TextDirection? textDirection, - VerticalDirection? verticalDirection, - TextBaseline? textBaseline, - }) { - return FlexLayoutProperties( - size: size ?? this.size, - children: children ?? this.children, - node: node, - constraints: constraints ?? this.constraints, - isFlex: isFlex ?? this.isFlex, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - flexFit: flexFit ?? this.flexFit, - direction: direction ?? this.direction, - mainAxisAlignment: mainAxisAlignment ?? this.mainAxisAlignment, - mainAxisSize: mainAxisSize ?? this.mainAxisSize, - crossAxisAlignment: crossAxisAlignment ?? this.crossAxisAlignment, - textDirection: textDirection ?? this.textDirection, - verticalDirection: verticalDirection ?? this.verticalDirection, - textBaseline: textBaseline ?? this.textBaseline, - ); - } - - static FlexLayoutProperties _buildNode(RemoteDiagnosticsNode node) { - final renderObjectJson = node.renderObject!.json; - final properties = (renderObjectJson['properties'] as List) - .cast>(); - - final data = { - for (final property in properties) - property['name']: property['description'] as String?, - }; - - return FlexLayoutProperties._fromNode( - node, - direction: _directionNamesToValues[data['direction']] ?? Axis.vertical, - mainAxisAlignment: - _mainAxisAlignmentNamesToValues[data['mainAxisAlignment']], - mainAxisSize: _mainAxisSizeNamesToValues[data['mainAxisSize']], - crossAxisAlignment: - _crossAxisAlignmentNamesToValues[data['crossAxisAlignment']], - textDirection: - _textDirectionNamesToValues[data['textDirection']] ?? - TextDirection.ltr, - verticalDirection: - _verticalDirectionNamesToValues[data['verticalDirection']] ?? - VerticalDirection.down, - textBaseline: _textBaselineNamesToValues[data['textBaseline']], - ); - } - - final Axis direction; - final MainAxisAlignment? mainAxisAlignment; - final CrossAxisAlignment? crossAxisAlignment; - final MainAxisSize? mainAxisSize; - final TextDirection textDirection; - final VerticalDirection verticalDirection; - final TextBaseline? textBaseline; - - List? _displayChildren; - - @override - List get displayChildren { - final displayChildren = _displayChildren; - if (displayChildren != null) return displayChildren; - return _displayChildren = startIsTopLeft - ? children - : children.reversed.toList(); - } - - int? _totalFlex; - - bool get isMainAxisHorizontal => direction == Axis.horizontal; - - bool get isMainAxisVertical => direction == Axis.vertical; - - String get horizontalDirectionDescription { - return direction == Axis.horizontal ? 'Main Axis' : 'Cross Axis'; - } - - String get verticalDirectionDescription { - return direction == Axis.vertical ? 'Main Axis' : 'Cross Axis'; - } - - String get type => direction.flexType; - - num get totalFlex { - if (children.isEmpty) return 0; - _totalFlex ??= children - .map((child) => child.flexFactor ?? 0) - .reduce((value, element) => value + element) - .toInt(); - return _totalFlex!; - } - - Axis get crossAxisDirection { - return direction == Axis.horizontal ? Axis.vertical : Axis.horizontal; - } - - double get mainAxisDimension => dimension(direction); - - double get crossAxisDimension => dimension(crossAxisDirection); - - @override - bool get isOverflowWidth { - if (direction == Axis.horizontal) { - return width + overflowEpsilon < sum(childrenWidths); - } - return width + overflowEpsilon < max(childrenWidths); - } - - @override - bool get isOverflowHeight { - if (direction == Axis.vertical) { - return height + overflowEpsilon < sum(childrenHeights); - } - return height + overflowEpsilon < max(childrenHeights); - } - - bool get startIsTopLeft { - switch (direction) { - case Axis.horizontal: - switch (textDirection) { - case TextDirection.ltr: - return true; - case TextDirection.rtl: - return false; - } - case Axis.vertical: - switch (verticalDirection) { - case VerticalDirection.down: - return true; - case VerticalDirection.up: - return false; - } - } - } - - /// render properties for laying out rendered Flex & Flex children widgets - /// the computation is similar to [RenderFlex].performLayout() method - List childrenRenderProperties({ - required double smallestRenderWidth, - required double largestRenderWidth, - required double smallestRenderHeight, - required double largestRenderHeight, - required double Function(Axis) maxSizeAvailable, - }) { - /// calculate the render empty spaces - final freeSpace = dimension(direction) - sum(childrenDimensions(direction)); - final displayMainAxisAlignment = startIsTopLeft - ? mainAxisAlignment - : mainAxisAlignment?.reversed; - - double leadingSpace(double freeSpace) { - if (children.isEmpty) return 0.0; - switch (displayMainAxisAlignment) { - case MainAxisAlignment.start: - case MainAxisAlignment.end: - return freeSpace; - case MainAxisAlignment.center: - return freeSpace * 0.5; - case MainAxisAlignment.spaceBetween: - return 0.0; - case MainAxisAlignment.spaceAround: - final spaceBetweenChildren = freeSpace / children.length; - return spaceBetweenChildren * 0.5; - case MainAxisAlignment.spaceEvenly: - return freeSpace / (children.length + 1); - default: - return 0.0; - } - } - - double betweenSpace(double freeSpace) { - if (children.isEmpty) return 0.0; - switch (displayMainAxisAlignment) { - case MainAxisAlignment.start: - case MainAxisAlignment.end: - case MainAxisAlignment.center: - return 0.0; - case MainAxisAlignment.spaceBetween: - if (children.length == 1) return freeSpace; - return freeSpace / (children.length - 1); - case MainAxisAlignment.spaceAround: - return freeSpace / children.length; - case MainAxisAlignment.spaceEvenly: - return freeSpace / (children.length + 1); - default: - return 0.0; - } - } - - double smallestRenderSize(Axis axis) { - return axis == Axis.horizontal - ? smallestRenderWidth - : smallestRenderHeight; - } - - double largestRenderSize(Axis axis) { - final lrs = axis == Axis.horizontal - ? largestRenderWidth - : largestRenderHeight; - // use all the space when visualizing cross axis - return (axis == direction) ? lrs : maxSizeAvailable(axis); - } - - List renderSizes(Axis axis) { - final sizes = childrenDimensions(axis); - if (freeSpace > 0.0 && axis == direction) { - /// include free space in the computation - sizes.add(freeSpace); - } - final smallestSize = min(sizes); - final largestSize = max(sizes); - if (axis == direction || - (crossAxisAlignment != CrossAxisAlignment.stretch && - smallestSize != largestSize)) { - return computeRenderSizes( - sizes: sizes, - smallestSize: smallestSize, - largestSize: largestSize, - smallestRenderSize: smallestRenderSize(axis), - largestRenderSize: largestRenderSize(axis), - maxSizeAvailable: maxSizeAvailable(axis), - ); - } else { - // uniform cross axis sizes. - double size = crossAxisAlignment == CrossAxisAlignment.stretch - ? maxSizeAvailable(axis) - : largestSize / - math.max(dimension(axis), 1.0) * - maxSizeAvailable(axis); - size = math.max(size, smallestRenderSize(axis)); - return sizes.map((_) => size).toList(); - } - } - - final widths = renderSizes(Axis.horizontal); - final heights = renderSizes(Axis.vertical); - - final renderFreeSpace = freeSpace > 0.0 - ? (isMainAxisHorizontal ? widths.last : heights.last) - : 0.0; - - final renderLeadingSpace = leadingSpace(renderFreeSpace); - final renderBetweenSpace = betweenSpace(renderFreeSpace); - - final childrenRenderProps = []; - - double lastMainAxisOffset() { - if (childrenRenderProps.isEmpty) return 0.0; - return childrenRenderProps.last.mainAxisOffset; - } - - double lastMainAxisDimension() { - if (childrenRenderProps.isEmpty) return 0.0; - return childrenRenderProps.last.mainAxisDimension; - } - - double space(int index) { - if (index == 0) { - if (displayMainAxisAlignment == MainAxisAlignment.start) return 0.0; - return renderLeadingSpace; - } - return renderBetweenSpace; - } - - double calculateMainAxisOffset(int i) { - return lastMainAxisOffset() + lastMainAxisDimension() + space(i); - } - - double calculateCrossAxisOffset(int i) { - final maxDimension = maxSizeAvailable(crossAxisDirection); - final usedDimension = crossAxisDirection == Axis.horizontal - ? widths[i] - : heights[i]; - - if (crossAxisAlignment == CrossAxisAlignment.start || - crossAxisAlignment == CrossAxisAlignment.stretch || - maxDimension == usedDimension) { - return 0.0; - } - final emptySpace = math.max(0.0, maxDimension - usedDimension); - if (crossAxisAlignment == CrossAxisAlignment.end) return emptySpace; - return emptySpace * 0.5; - } - - for (var i = 0; i < children.length; ++i) { - childrenRenderProps.add( - RenderProperties( - axis: direction, - size: Size(widths[i], heights[i]), - offset: Offset.zero, - realSize: displayChildren[i].size, - layoutProperties: displayChildren[i], - ) - ..mainAxisOffset = calculateMainAxisOffset(i) - ..crossAxisOffset = calculateCrossAxisOffset(i), - ); - } - - final spaces = []; - final actualLeadingSpace = leadingSpace(freeSpace); - final actualBetweenSpace = betweenSpace(freeSpace); - final renderPropsWithFullCrossAxisDimension = - RenderProperties( - axis: direction, - isFreeSpace: true, - layoutProperties: this, - ) - ..crossAxisDimension = maxSizeAvailable(crossAxisDirection) - ..crossAxisRealDimension = dimension(crossAxisDirection) - ..crossAxisOffset = 0.0; - if (actualLeadingSpace > 0.0 && - displayMainAxisAlignment != MainAxisAlignment.start) { - spaces.add( - renderPropsWithFullCrossAxisDimension.copyWith() - ..mainAxisOffset = 0.0 - ..mainAxisDimension = renderLeadingSpace - ..mainAxisRealDimension = actualLeadingSpace, - ); - } - if (actualBetweenSpace > 0.0) { - for (var i = 0; i < childrenRenderProps.length - 1; ++i) { - final child = childrenRenderProps[i]; - spaces.add( - renderPropsWithFullCrossAxisDimension.copyWith() - ..mainAxisDimension = renderBetweenSpace - ..mainAxisRealDimension = actualBetweenSpace - ..mainAxisOffset = child.mainAxisOffset + child.mainAxisDimension, - ); - } - } - if (actualLeadingSpace > 0.0 && - displayMainAxisAlignment != MainAxisAlignment.end) { - spaces.add( - renderPropsWithFullCrossAxisDimension.copyWith() - ..mainAxisOffset = - childrenRenderProps.last.mainAxisDimension + - childrenRenderProps.last.mainAxisOffset - ..mainAxisDimension = renderLeadingSpace - ..mainAxisRealDimension = actualLeadingSpace, - ); - } - return [...childrenRenderProps, ...spaces]; - } - - List crossAxisSpaces({ - required List childrenRenderProperties, - required double Function(Axis) maxSizeAvailable, - }) { - if (crossAxisAlignment == CrossAxisAlignment.stretch) return []; - final spaces = []; - for (var i = 0; i < children.length; ++i) { - if (dimension(crossAxisDirection) == - displayChildren[i].dimension(crossAxisDirection) || - childrenRenderProperties[i].crossAxisDimension == - maxSizeAvailable(crossAxisDirection)) { - continue; - } - - final renderProperties = childrenRenderProperties[i]; - final space = renderProperties.copyWith(isFreeSpace: true); - - space.crossAxisRealDimension = - crossAxisDimension - space.crossAxisRealDimension; - space.crossAxisDimension = - maxSizeAvailable(crossAxisDirection) - space.crossAxisDimension; - if (space.crossAxisDimension <= 0.0) continue; - if (crossAxisAlignment == CrossAxisAlignment.center) { - space.crossAxisDimension *= 0.5; - final crossAxisRealDimension = space.crossAxisRealDimension; - space.crossAxisRealDimension = crossAxisRealDimension * 0.5; - spaces.add(space.copyWith()..crossAxisOffset = 0.0); - spaces.add( - space.copyWith() - ..crossAxisOffset = - renderProperties.crossAxisDimension + - renderProperties.crossAxisOffset, - ); - } else { - space.crossAxisOffset = crossAxisAlignment == CrossAxisAlignment.end - ? 0 - : renderProperties.crossAxisDimension; - spaces.add(space); - } - } - return spaces; - } - - static final _directionNamesToValues = Axis.values.asNameMap(); - static final _mainAxisAlignmentNamesToValues = MainAxisAlignment.values - .asNameMap(); - static final _mainAxisSizeNamesToValues = MainAxisSize.values.asNameMap(); - static final _crossAxisAlignmentNamesToValues = CrossAxisAlignment.values - .asNameMap(); - static final _textDirectionNamesToValues = TextDirection.values.asNameMap(); - static final _verticalDirectionNamesToValues = VerticalDirection.values - .asNameMap(); - static final _textBaselineNamesToValues = TextBaseline.values.asNameMap(); -} - -/// Information for rendering a [LayoutProperties] node. -class RenderProperties { - RenderProperties({ - required this.axis, - required this.layoutProperties, - this.isFreeSpace = false, - Size? size, - Offset? offset, - Size? realSize, - }) : width = size?.width ?? 0.0, - height = size?.height ?? 0.0, - realWidth = realSize?.width ?? 0.0, - realHeight = realSize?.height ?? 0.0, - dx = offset?.dx ?? 0.0, - dy = offset?.dy ?? 0.0; - - final Axis axis; - - /// Represents which node is rendered for this object. - final LayoutProperties layoutProperties; - - final bool isFreeSpace; - - double dx, dy; - double width, height; - double realWidth, realHeight; - - Size get size => Size(width, height); - - Size get realSize => Size(realWidth, realHeight); - - Offset get offset => Offset(dx, dy); - - double get mainAxisDimension => axis == Axis.horizontal ? width : height; - - set mainAxisDimension(double dim) { - if (axis == Axis.horizontal) { - width = dim; - } else { - height = dim; - } - } - - double get crossAxisDimension => axis == Axis.horizontal ? height : width; - - set crossAxisDimension(double dim) { - if (axis == Axis.horizontal) { - height = dim; - } else { - width = dim; - } - } - - double get mainAxisOffset => axis == Axis.horizontal ? dx : dy; - - set mainAxisOffset(double offset) { - if (axis == Axis.horizontal) { - dx = offset; - } else { - dy = offset; - } - } - - double get crossAxisOffset => axis == Axis.horizontal ? dy : dx; - - set crossAxisOffset(double offset) { - if (axis == Axis.horizontal) { - dy = offset; - } else { - dx = offset; - } - } - - double get mainAxisRealDimension => - axis == Axis.horizontal ? realWidth : realHeight; - - set mainAxisRealDimension(double newVal) { - if (axis == Axis.horizontal) { - realWidth = newVal; - } else { - realHeight = newVal; - } - } - - double get crossAxisRealDimension => - axis == Axis.horizontal ? realHeight : realWidth; - - set crossAxisRealDimension(double newVal) { - if (axis == Axis.horizontal) { - realHeight = newVal; - } else { - realWidth = newVal; - } - } - - RenderProperties copyWith({bool? isFreeSpace}) { - return RenderProperties( - axis: axis, - size: size, - offset: offset, - realSize: realSize, - layoutProperties: layoutProperties, - isFreeSpace: isFreeSpace ?? this.isFreeSpace, - ); - } - - @override - int get hashCode => - axis.hashCode ^ - size.hashCode ^ - offset.hashCode ^ - realSize.hashCode ^ - isFreeSpace.hashCode; - - @override - bool operator ==(Object other) { - return other is RenderProperties && - axis == other.axis && - size.closeTo(other.size) && - offset.closeTo(other.offset) && - realSize.closeTo(other.realSize) && - isFreeSpace == other.isFreeSpace; - } - - @override - String toString() { - return '{ axis: $axis, size: $size, offset: $offset, realSize: $realSize, isFreeSpace: $isFreeSpace }'; - } -} - -bool _closeTo(double a, double b, {int precision = 1}) { - return a.toStringAsPrecision(precision) == b.toStringAsPrecision(precision); -} - -extension on Size { - bool closeTo(Size other) { - return _closeTo(width, other.width) && _closeTo(height, other.height); - } -} - -extension on Offset { - bool closeTo(Offset other) { - return _closeTo(dx, other.dx) && _closeTo(dy, other.dy); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart deleted file mode 100644 index 5ab577d564d..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_body.dart +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; -import 'dart:collection'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; - -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/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree.dart'; -import '../../shared/globals.dart'; -import '../../shared/managers/banner_messages.dart'; -import '../../shared/managers/error_badge_manager.dart'; -import '../../shared/primitives/blocking_action_mixin.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/search.dart'; -import '../inspector_shared/inspector_controls.dart'; -import '../inspector_shared/inspector_screen.dart'; -import '../inspector_shared/inspector_settings_dialog.dart'; -import 'inspector_controller.dart'; -import 'inspector_screen_details_tab.dart'; -import 'inspector_tree_controller.dart'; - -class InspectorScreenBody extends StatefulWidget { - const InspectorScreenBody({super.key, required this.controller}); - - final InspectorController controller; - - @override - InspectorScreenBodyState createState() => InspectorScreenBodyState(); -} - -class InspectorScreenBodyState extends State - with BlockingActionMixin, AutoDisposeMixin { - InspectorController get controller => widget.controller; - - InspectorTreeController get _summaryTreeController => - controller.inspectorTree; - - InspectorTreeController get _detailsTreeController => - controller.details!.inspectorTree; - - bool searchVisible = false; - - SearchControllerMixin get searchController => _summaryTreeController; - - /// Indicates whether search can be closed. The value is set to true when - /// search target type dropdown is displayed - /// TODO(https://github.com/flutter/devtools/issues/3489) use this variable when adding the scope dropdown - bool searchPreventClose = false; - - SearchTargetType searchTarget = SearchTargetType.widget; - - static const summaryTreeKey = Key('Summary Tree'); - static const detailsTreeKey = Key('Details Tree'); - static const minScreenWidthForText = 900.0; - static const serviceExtensionButtonsIncludeTextWidth = 1200.0; - - @override - void initState() { - super.initState(); - ga.screen(InspectorScreen.id); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - if (serviceConnection.inspectorService == null) { - // The app must not be a Flutter app. - return; - } - - cancelListeners(); - searchVisible = searchController.search.isNotEmpty; - addAutoDisposeListener(searchController.searchFieldFocusNode, () { - final searchFieldFocusNode = searchController.searchFieldFocusNode; - if (searchFieldFocusNode == null) return; - // Close the search once focus is lost and following conditions are met: - // 1. Search string is empty. - // 2. [searchPreventClose] == false (this is set true when searchTargetType Dropdown is opened). - if (!searchFieldFocusNode.hasFocus && - searchController.search.isEmpty && - !searchPreventClose) { - setState(() { - searchVisible = false; - }); - } - - // Reset [searchPreventClose] state to false after the search field gains focus. - // Focus is returned automatically once the Dropdown menu is closed. - if (searchFieldFocusNode.hasFocus) { - searchPreventClose = false; - } - }); - addAutoDisposeListener(preferences.inspector.pubRootDirectories, () { - if (serviceConnection.serviceManager.connectedState.value.connected && - controller.firstInspectorTreeLoadCompleted) { - _refreshInspector(); - } - }); - - if (!controller.firstInspectorTreeLoadCompleted) { - ga.timeStart(InspectorScreen.id, gac.pageReady); - } - - _summaryTreeController.setSearchTarget(searchTarget); - - _showLegacyInspectorWarning(context); - } - - @override - Widget build(BuildContext context) { - final summaryTree = _buildSummaryTreeColumn(); - - final detailsTree = InspectorTree( - key: detailsTreeKey, - controller: controller, - treeController: _detailsTreeController, - summaryTreeController: _summaryTreeController, - screenId: InspectorScreen.id, - ); - - final splitAxis = SplitPane.axisFor(context, 0.85); - final widgetTrees = SplitPane( - axis: splitAxis, - initialFractions: const [0.33, 0.67], - children: [ - summaryTree, - InspectorDetails(detailsTree: detailsTree, controller: controller), - ], - ); - return Column( - children: [ - const InspectorControls(), - const SizedBox(height: intermediateSpacing), - Expanded(child: widgetTrees), - ], - ); - } - - Widget _buildSummaryTreeColumn() { - return LayoutBuilder( - builder: (context, constraints) { - return RoundedOutlinedBorder( - child: Column( - children: [ - InspectorSummaryTreeControls( - isSearchVisible: searchVisible, - constraints: constraints, - onRefreshInspectorPressed: _refreshInspector, - onSearchVisibleToggle: _onSearchVisibleToggle, - searchFieldBuilder: () => - StatelessSearchField( - controller: _summaryTreeController, - searchFieldEnabled: true, - shouldRequestFocus: searchVisible, - supportsNavigation: true, - onClose: _onSearchVisibleToggle, - ), - ), - Expanded( - child: ValueListenableBuilder( - valueListenable: serviceConnection.errorBadgeManager - .erroredItemsForPage(InspectorScreen.id), - builder: (_, LinkedHashMap errors, _) { - final inspectableErrors = - errors.map( - (key, value) => MapEntry( - key, - value as InspectableWidgetError, - ), - ) - as LinkedHashMap; - return Stack( - children: [ - InspectorTree( - key: summaryTreeKey, - controller: controller, - treeController: _summaryTreeController, - isSummaryTree: true, - widgetErrors: inspectableErrors, - screenId: InspectorScreen.id, - ), - if (errors.isNotEmpty) - ValueListenableBuilder( - valueListenable: controller.selectedErrorIndex, - builder: (_, selectedErrorIndex, _) => Positioned( - top: 0, - right: 0, - child: ErrorNavigator( - errors: inspectableErrors, - errorIndex: selectedErrorIndex, - onSelectError: controller.selectErrorByIndex, - ), - ), - ), - ], - ); - }, - ), - ), - ], - ), - ); - }, - ); - } - - void _onSearchVisibleToggle() { - setState(() { - searchVisible = !searchVisible; - }); - _summaryTreeController.resetSearch(); - } - - void _showLegacyInspectorWarning(BuildContext context) { - if (context.mounted) { - pushLegacyInspectorWarning(InspectorScreen.id); - } - } - - List getServiceExtensionWidgets() { - return [ - ServiceExtensionButtonGroup( - minScreenWidthForText: 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, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), - ); - unawaited( - blockWhileInProgress(() async { - // If the user is force refreshing the inspector before the first load has - // completed, this could indicate a slow load time or that the inspector - // failed to load the tree once available. - if (!controller.firstInspectorTreeLoadCompleted) { - // We do not want to complete this timing operation because the force - // refresh will skew the results. - ga.cancelTimingOperation(InspectorScreen.id, gac.pageReady); - ga.select( - gac.inspector, - gac.refreshEmptyTree, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), - ); - controller.firstInspectorTreeLoadCompleted = true; - } - await controller.onForceRefresh(); - }), - ); - } -} - -class InspectorSummaryTreeControls extends StatelessWidget { - const InspectorSummaryTreeControls({ - super.key, - required this.constraints, - required this.isSearchVisible, - required this.onRefreshInspectorPressed, - required this.onSearchVisibleToggle, - required this.searchFieldBuilder, - }); - - static const _searchBreakpoint = 375.0; - - final bool isSearchVisible; - final BoxConstraints constraints; - final VoidCallback onRefreshInspectorPressed; - final VoidCallback onSearchVisibleToggle; - final Widget Function() searchFieldBuilder; - - @override - Widget build(BuildContext context) { - return Column( - children: [ - _controlsContainer( - context, - Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: denseSpacing), - child: Text( - 'Widget Tree', - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ...!isSearchVisible - ? [ - const Spacer(), - ToolbarAction( - icon: Icons.search, - onPressed: onSearchVisibleToggle, - tooltip: 'Search Tree', - ), - ] - : [ - constraints.maxWidth >= _searchBreakpoint - ? _buildSearchControls() - : const Spacer(), - ], - ToolbarAction( - icon: Icons.refresh, - onPressed: onRefreshInspectorPressed, - tooltip: 'Refresh Tree', - ), - ], - ), - ), - if (isSearchVisible && constraints.maxWidth < _searchBreakpoint) - _controlsContainer(context, Row(children: [_buildSearchControls()])), - ], - ); - } - - Container _controlsContainer(BuildContext context, Widget child) { - return Container( - height: defaultHeaderHeight, - decoration: BoxDecoration( - border: Border(bottom: defaultBorderSide(Theme.of(context))), - ), - child: child, - ); - } - - Widget _buildSearchControls() { - return Expanded( - child: SizedBox( - height: defaultTextFieldHeight, - child: searchFieldBuilder(), - ), - ); - } -} - -class ErrorNavigator extends StatelessWidget { - const ErrorNavigator({ - super.key, - required this.errors, - required this.errorIndex, - required this.onSelectError, - }); - - final LinkedHashMap errors; - - final int? errorIndex; - - final void Function(int) onSelectError; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final label = errorIndex != null - ? 'Error ${errorIndex! + 1}/${errors.length}' - : 'Errors: ${errors.length}'; - return Container( - color: colorScheme.errorContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: defaultSpacing, - vertical: denseSpacing, - ), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: denseSpacing), - child: Text( - label, - style: TextStyle(color: colorScheme.onErrorContainer), - ), - ), - _ErrorNavigatorButton( - icon: Icons.keyboard_arrow_up, - onPressed: _previousError, - ), - _ErrorNavigatorButton( - icon: Icons.keyboard_arrow_down, - onPressed: _nextError, - ), - ], - ), - ), - ); - } - - void _previousError() { - var newIndex = errorIndex == null ? errors.length - 1 : errorIndex! - 1; - while (newIndex < 0) { - newIndex += errors.length; - } - - onSelectError(newIndex); - } - - void _nextError() { - final newIndex = errorIndex == null ? 0 : (errorIndex! + 1) % errors.length; - - onSelectError(newIndex); - } -} - -class _ErrorNavigatorButton extends StatelessWidget { - const _ErrorNavigatorButton({required this.icon, required this.onPressed}); - - final IconData icon; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return SizedBox( - // This is required to force the button size. - height: defaultButtonHeight, - width: defaultButtonHeight, - child: IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - splashRadius: defaultIconSize, - icon: Icon(icon), - color: Theme.of(context).colorScheme.onErrorContainer, - onPressed: onPressed, - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart deleted file mode 100644 index ae8cc53e551..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_screen_details_tab.dart +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/globals.dart'; -import '../../shared/preferences/preferences.dart'; -import '../../shared/primitives/blocking_action_mixin.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/tab.dart'; -import 'inspector_controller.dart'; -import 'inspector_screen_body.dart'; -import 'layout_explorer/layout_explorer.dart'; - -class InspectorDetails extends StatelessWidget { - const InspectorDetails({ - required this.detailsTree, - required this.controller, - super.key, - }); - - final Widget detailsTree; - final InspectorController controller; - - @override - Widget build(BuildContext context) { - final tabs = [ - ( - tab: _buildTab(tabName: InspectorDetailsViewType.layoutExplorer.key), - tabView: LayoutExplorerTab(controller: controller), - ), - ( - tab: _buildTab( - tabName: InspectorDetailsViewType.widgetDetailsTree.key, - trailing: InspectorExpandCollapseButtons(controller: controller), - ), - tabView: detailsTree, - ), - ]; - return ValueListenableBuilder( - valueListenable: preferences.inspector.defaultDetailsView, - builder: (BuildContext context, value, Widget? child) { - int defaultInspectorViewIndex = 0; - - if (preferences.inspector.defaultDetailsView.value == - InspectorDetailsViewType.widgetDetailsTree) { - defaultInspectorViewIndex = 1; - } - - return AnalyticsTabbedView( - tabs: tabs, - gaScreen: gac.inspector, - initialSelectedIndex: defaultInspectorViewIndex, - ); - }, - ); - } - - DevToolsTab _buildTab({required String tabName, Widget? trailing}) { - return DevToolsTab.create( - tabName: tabName, - gaPrefix: 'inspectorDetailsTab', - trailing: trailing, - ); - } -} - -class InspectorExpandCollapseButtons extends StatefulWidget { - const InspectorExpandCollapseButtons({super.key, required this.controller}); - - final InspectorController controller; - - @override - State createState() => - _InspectorExpandCollapseButtonsState(); -} - -class _InspectorExpandCollapseButtonsState - extends State - with BlockingActionMixin { - bool get enableButtons => !actionInProgress; - - @override - Widget build(BuildContext context) { - return Container( - alignment: Alignment.centerRight, - decoration: BoxDecoration( - border: Border(left: defaultBorderSide(Theme.of(context))), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - child: GaDevToolsButton( - icon: Icons.unfold_more, - onPressed: enableButtons ? _onExpandClick : null, - label: 'Expand all', - minScreenWidthForText: - InspectorScreenBodyState.minScreenWidthForText, - gaScreen: gac.inspector, - gaSelection: gac.expandAll, - outlined: false, - ), - ), - const SizedBox(width: denseSpacing), - SizedBox( - child: GaDevToolsButton( - icon: Icons.unfold_less, - onPressed: enableButtons ? _onCollapseClick : null, - label: 'Collapse to selected', - minScreenWidthForText: - InspectorScreenBodyState.minScreenWidthForText, - gaScreen: gac.inspector, - gaSelection: gac.collapseAll, - outlined: false, - ), - ), - ], - ), - ); - } - - void _onExpandClick() { - unawaited( - blockWhileInProgress(() async { - ga.select(gac.inspector, gac.expandAll); - await widget.controller.expandAllNodesInDetailsTree(); - }), - ); - } - - void _onCollapseClick() { - ga.select(gac.inspector, gac.collapseAll); - widget.controller.collapseDetailsToSelected(); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart deleted file mode 100644 index 35b5d9b66cd..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_tree_controller.dart +++ /dev/null @@ -1,1331 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; -import 'dart:collection'; -import 'dart:math'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:logging/logging.dart'; - -import '../../shared/analytics/analytics.dart' as ga; -import '../../shared/analytics/constants.dart' as gac; -import '../../shared/analytics/metrics.dart'; -import '../../shared/console/eval/inspector_tree.dart'; -import '../../shared/console/widgets/description.dart'; -import '../../shared/diagnostics/diagnostics_node.dart'; -import '../../shared/globals.dart'; -import '../../shared/managers/error_badge_manager.dart'; -import '../../shared/primitives/collapsible_mixin.dart'; -import '../../shared/primitives/diagnostics_text_styles.dart'; -import '../../shared/primitives/utils.dart'; -import '../../shared/ui/colors.dart'; -import '../../shared/ui/common_widgets.dart'; -import '../../shared/ui/search.dart'; -import '../../shared/ui/utils.dart'; -import '../../shared/utils/utils.dart'; -import 'inspector_breadcrumbs.dart'; -import 'inspector_controller.dart'; - -final _log = Logger('inspector_tree_controller'); - -/// Presents a [InspectorTreeNode]. -class _InspectorTreeRowWidget extends StatefulWidget { - /// Constructs a [_InspectorTreeRowWidget] that presents a line in the - /// Inspector tree. - const _InspectorTreeRowWidget({ - required super.key, - required this.row, - required this.inspectorTreeState, - this.error, - required this.scrollControllerX, - required this.viewportWidth, - }); - - final _InspectorTreeState inspectorTreeState; - - InspectorTreeNode get node => row.node; - final InspectorTreeRow row; - final ScrollController scrollControllerX; - final double viewportWidth; - - /// A [DevToolsError] that applies to the widget in this row. - /// - /// This will be null if there is no error for this row. - final DevToolsError? error; - - @override - _InspectorTreeRowState createState() => _InspectorTreeRowState(); -} - -class _InspectorTreeRowState extends State<_InspectorTreeRowWidget> - with TickerProviderStateMixin, CollapsibleAnimationMixin { - @override - Widget build(BuildContext context) { - return SizedBox( - height: inspectorRowHeight, - child: InspectorRowContent( - row: widget.row, - error: widget.error, - expandArrowAnimation: expandArrowAnimation, - controller: widget.inspectorTreeState.treeController!, - scrollControllerX: widget.scrollControllerX, - viewportWidth: widget.viewportWidth, - onToggle: () { - setExpanded(!isExpanded); - }, - ), - ); - } - - @override - bool get isExpanded => widget.node.isExpanded; - - @override - void onExpandChanged(bool expanded) { - setState(() { - final row = widget.row; - if (expanded) { - widget.inspectorTreeState.treeController!.onExpandRow(row); - } else { - widget.inspectorTreeState.treeController!.onCollapseRow(row); - } - }); - } - - @override - bool shouldShow() => widget.node.shouldShow; -} - -class InspectorTreeController extends DisposableController - with SearchControllerMixin { - InspectorTreeController({this.gaId}) { - init(); - } - - /// Clients the controller notifies to trigger changes to the UI. - final _clients = {}; - - /// Identifier used when sending Google Analytics about events in this - /// [InspectorTreeController]. - final int? gaId; - - InspectorTreeNode createNode() => InspectorTreeNode(); - - SearchTargetType _searchTarget = SearchTargetType.widget; - int _rootSetCount = 0; - - @override - void init() { - super.init(); - ga.select( - gac.inspector, - gac.inspectorTreeControllerInitialized, - nonInteraction: true, - screenMetricsProvider: () => InspectorScreenMetrics.legacy( - inspectorTreeControllerId: gaId, - rootSetCount: _rootSetCount, - rowCount: _root?.subtreeSize, - ), - ); - } - - void addClient(InspectorControllerClient value) { - final firstClient = _clients.isEmpty; - _clients.add(value); - if (firstClient) { - config.onClientActiveChange?.call(true); - } - } - - void removeClient(InspectorControllerClient value) { - _clients.remove(value); - if (_clients.isEmpty) { - config.onClientActiveChange?.call(false); - } - } - - // Method defined to avoid a direct Flutter dependency. - void setState(VoidCallback fn) { - fn(); - for (final client in _clients) { - client.onChanged(); - } - } - - void requestFocus() { - for (final client in _clients) { - client.requestFocus(); - } - } - - InspectorTreeNode? get root => _root; - InspectorTreeNode? _root; - - set root(InspectorTreeNode? node) { - if (disposed) return; - - setState(() { - _root = node; - _populateSearchableCachedRows(); - - ga.select( - gac.inspector, - gac.inspectorTreeControllerRootChange, - nonInteraction: true, - screenMetricsProvider: () => InspectorScreenMetrics.legacy( - inspectorTreeControllerId: gaId, - rootSetCount: ++_rootSetCount, - rowCount: _root?.subtreeSize, - ), - ); - }); - } - - InspectorTreeNode? get selection => _selection; - InspectorTreeNode? _selection; - - late final InspectorTreeConfig config; - - set selection(InspectorTreeNode? node) { - if (node == _selection) return; - - setState(() { - _selection?.selected = false; - _selection = node; - _selection?.selected = true; - final configLocal = config; - if (configLocal.onSelectionChange != null) { - configLocal.onSelectionChange!(); - } - }); - } - - InspectorTreeNode? get hover => _hover; - InspectorTreeNode? _hover; - - double? lastContentWidth; - - final cachedRows = []; - InspectorTreeRow? _cachedSelectedRow; - - /// All cached rows of the tree. - /// - /// Similar to [cachedRows] but: - /// * contains every row in the tree (including collapsed rows) - /// * items don't change when nodes are expanded or collapsed - /// * items are populated only when root is changed - final _searchableCachedRows = []; - - void setSearchTarget(SearchTargetType searchTarget) { - _searchTarget = searchTarget; - refreshSearchMatches(); - } - - // TODO: we should add a listener instead that clears the cache when the - // root is marked as dirty. - void _maybeClearCache() { - final rootLocal = root; - if (rootLocal != null && rootLocal.isDirty) { - cachedRows.clear(); - _cachedSelectedRow = null; - rootLocal.isDirty = false; - lastContentWidth = null; - } - } - - void _populateSearchableCachedRows() { - _searchableCachedRows.clear(); - for (int i = 0; i < numRows; i++) { - _searchableCachedRows.add(getCachedRow(i)); - } - } - - InspectorTreeRow? getCachedRow(int index) { - if (index < 0) return null; - - _maybeClearCache(); - while (cachedRows.length <= index) { - cachedRows.add(null); - } - cachedRows[index] ??= root?.getRow(index); - - final cachedRow = cachedRows[index]; - cachedRow?.isSearchMatch = - _searchableCachedRows.safeGet(index)?.isSearchMatch ?? false; - - if (cachedRow?.isSelected == true) { - _cachedSelectedRow = cachedRow; - } - return cachedRow; - } - - double getRowOffset(int index) { - return (getCachedRow(index)?.depth ?? 0) * inspectorColumnWidth; - } - - List getPathFromSelectedRowToRoot() { - final selectedItem = _cachedSelectedRow?.node; - if (selectedItem == null) return []; - - final pathToRoot = [selectedItem]; - InspectorTreeNode? nextParentNode = selectedItem.parent; - while (nextParentNode != null) { - pathToRoot.add(nextParentNode); - nextParentNode = nextParentNode.parent; - } - return pathToRoot.reversed.toList(); - } - - set hover(InspectorTreeNode? node) { - if (node == _hover) { - return; - } - setState(() { - _hover = node; - // TODO(jacobr): we could choose to repaint only a portion of the UI - }); - } - - void navigateUp() { - _navigateHelper(-1); - } - - void navigateDown() { - _navigateHelper(1); - } - - void navigateLeft() { - final selectionLocal = selection; - - // This logic is consistent with how IntelliJ handles tree navigation on - // on left arrow key press. - if (selectionLocal == null) { - _navigateHelper(-1); - return; - } - - if (selectionLocal.isExpanded) { - setState(() { - selectionLocal.isExpanded = false; - }); - return; - } - if (selectionLocal.parent != null) { - selection = selectionLocal.parent; - } - } - - void navigateRight() { - // This logic is consistent with how IntelliJ handles tree navigation on - // on right arrow key press. - - final selectionLocal = selection; - - if (selectionLocal == null || selectionLocal.isExpanded) { - _navigateHelper(1); - return; - } - - setState(() { - selectionLocal.isExpanded = true; - }); - } - - void _navigateHelper(int indexOffset) { - if (numRows == 0) return; - - if (selection == null) { - selection = root; - return; - } - - final rootLocal = root!; - - selection = rootLocal - .getRow( - (rootLocal.getRowIndex(selection!) + indexOffset).clamp( - 0, - numRows - 1, - ), - ) - ?.node; - } - - static const horizontalPadding = 10.0; - - double getDepthIndent(int depth) { - return (depth + 1) * inspectorColumnWidth + horizontalPadding; - } - - double rowYTop(int index) { - return inspectorRowHeight * index; - } - - void nodeChanged(InspectorTreeNode node) { - setState(() { - node.isDirty = true; - }); - } - - void removeNodeFromParent(InspectorTreeNode node) { - setState(() { - node.parent?.removeChild(node); - }); - } - - void appendChild(InspectorTreeNode node, InspectorTreeNode child) { - setState(() { - node.appendChild(child); - }); - } - - void expandPath(InspectorTreeNode? node) { - setState(() { - _expandPath(node); - }); - } - - void _expandPath(InspectorTreeNode? node) { - while (node != null) { - if (!node.isExpanded) { - node.isExpanded = true; - } - node = node.parent; - } - } - - void collapseToSelected() { - setState(() { - _collapseAllNodes(root!); - if (selection == null) return; - _expandPath(selection); - }); - } - - void _collapseAllNodes(InspectorTreeNode root) { - root.isExpanded = false; - root.children.forEach(_collapseAllNodes); - } - - int get numRows => root?.subtreeSize ?? 0; - - int getRowIndex(double y) => max(0, y ~/ inspectorRowHeight); - - InspectorTreeRow? getRowForNode(InspectorTreeNode node) { - final rootLocal = root; - if (rootLocal == null) return null; - return getCachedRow(rootLocal.getRowIndex(node)); - } - - InspectorTreeRow? getRow(Offset offset) { - final rootLocal = root; - if (rootLocal == null) return null; - final row = getRowIndex(offset.dy); - return row < rootLocal.subtreeSize ? getCachedRow(row) : null; - } - - void onExpandRow(InspectorTreeRow row) { - setState(() { - final onExpand = config.onExpand; - row.node.isExpanded = true; - if (onExpand != null) { - onExpand(row.node); - } - }); - } - - void onCollapseRow(InspectorTreeRow row) { - setState(() { - row.node.isExpanded = false; - }); - } - - void onSelectRow(InspectorTreeRow row) { - onSelectNode(row.node); - } - - void onSelectNode(InspectorTreeNode? node) { - selection = node; - ga.select( - gac.inspector, - gac.treeNodeSelection, - screenMetricsProvider: () => InspectorScreenMetrics.legacy(), - ); - expandPath(node); - } - - Rect getBoundingBox(InspectorTreeRow row) { - // For future reference: the bounding box likely needs to be in terms of - // positions after the current animations are complete so that computations - // to start animations to show specific widget scroll to where the target - // nodes will be displayed rather than where they are currently displayed. - final diagnostic = row.node.diagnostic; - // The node width is approximated since the widgets are not available at the - // time of calculating the bounding box. - final approximateNodeWidth = - DiagnosticsNodeDescription.approximateNodeWidth(diagnostic); - return Rect.fromLTWH( - getDepthIndent(row.depth), - rowYTop(row.index), - approximateNodeWidth, - inspectorRowHeight, - ); - } - - void scrollToRect(Rect targetRect) { - for (final client in _clients) { - client.scrollToRect(targetRect); - } - } - - /// Width each row in the tree should have ignoring its indent. - /// - /// Content in rows should wrap if it exceeds this width. - final rowWidth = 1200; - - /// Maximum indent of the tree in pixels. - double? _maxIndent; - - double get maxRowIndent { - if (lastContentWidth == null) { - double maxIndent = 0; - for (int i = 0; i < numRows; i++) { - final row = getCachedRow(i); - if (row != null) { - maxIndent = max(maxIndent, getDepthIndent(row.depth)); - } - } - lastContentWidth = maxIndent + maxIndent; - _maxIndent = maxIndent; - } - return _maxIndent!; - } - - void animateToTargets(List targets) { - Rect? targetRect; - - for (final target in targets) { - final row = getRowForNode(target); - if (row != null) { - final rowRect = getBoundingBox(row); - targetRect = targetRect == null - ? rowRect - : targetRect.expandToInclude(rowRect); - } - } - - if (targetRect == null || targetRect.isEmpty) return; - - scrollToRect(targetRect); - } - - bool expandPropertiesByDefault(DiagnosticsTreeStyle style) { - // This code matches the text style defaults for which styles are - // by default and which aren't. - switch (style) { - case DiagnosticsTreeStyle.none: - case DiagnosticsTreeStyle.singleLine: - case DiagnosticsTreeStyle.errorProperty: - return false; - - case DiagnosticsTreeStyle.sparse: - case DiagnosticsTreeStyle.offstage: - case DiagnosticsTreeStyle.dense: - case DiagnosticsTreeStyle.transition: - case DiagnosticsTreeStyle.error: - case DiagnosticsTreeStyle.whitespace: - case DiagnosticsTreeStyle.flat: - case DiagnosticsTreeStyle.shallow: - case DiagnosticsTreeStyle.truncateChildren: - return true; - } - } - - InspectorTreeNode setupInspectorTreeNode( - InspectorTreeNode node, - RemoteDiagnosticsNode diagnosticsNode, { - required bool expandChildren, - required bool expandProperties, - }) { - node.diagnostic = diagnosticsNode; - final configLocal = config; - if (configLocal.onNodeAdded != null) { - configLocal.onNodeAdded!(node, diagnosticsNode); - } - - if (diagnosticsNode.hasChildren || - diagnosticsNode.inlineProperties.isNotEmpty) { - if (diagnosticsNode.childrenReady || !diagnosticsNode.hasChildren) { - final styleIsMultiline = expandPropertiesByDefault( - diagnosticsNode.style, - ); - setupChildren( - diagnosticsNode, - node, - node.diagnostic!.childrenNow, - expandChildren: expandChildren && styleIsMultiline, - expandProperties: expandProperties && styleIsMultiline, - ); - } else { - node.clearChildren(); - node.appendChild(createNode()); - } - } - return node; - } - - void setupChildren( - RemoteDiagnosticsNode parent, - InspectorTreeNode treeNode, - List? children, { - required bool expandChildren, - required bool expandProperties, - }) { - treeNode.isExpanded = expandChildren; - if (treeNode.children.isNotEmpty) { - // Only case supported is this is the loading node. - assert(treeNode.children.length == 1); - removeNodeFromParent(treeNode.children.first); - } - final inlineProperties = parent.inlineProperties; - - for (final property in inlineProperties) { - appendChild( - treeNode, - setupInspectorTreeNode( - createNode(), - property, - // We are inside a property so only expand children if - // expandProperties is true. - expandChildren: expandProperties, - expandProperties: expandProperties, - ), - ); - } - if (children != null) { - for (final child in children) { - appendChild( - treeNode, - setupInspectorTreeNode( - createNode(), - child, - expandChildren: expandChildren, - expandProperties: expandProperties, - ), - ); - } - } - } - - Future maybePopulateChildren(InspectorTreeNode treeNode) async { - final diagnostic = treeNode.diagnostic; - if (diagnostic != null && - diagnostic.hasChildren && - (treeNode.hasPlaceholderChildren || treeNode.children.isEmpty)) { - try { - final children = await diagnostic.children; - if (treeNode.hasPlaceholderChildren || treeNode.children.isEmpty) { - setupChildren( - diagnostic, - treeNode, - children, - expandChildren: true, - expandProperties: false, - ); - nodeChanged(treeNode); - if (treeNode == selection) { - expandPath(treeNode); - } - } - } catch (e, st) { - _log.shout(e, e, st); - } - } - } - - /* Search support */ - @override - void onMatchChanged(int index) { - onSelectRow(searchMatches.value[index]); - } - - @override - Duration get debounceDelay => const Duration(milliseconds: 300); - - @override - List matchesForSearch( - String search, { - bool searchPreviousMatches = false, - }) { - final matches = []; - - if (searchPreviousMatches) { - final previousMatches = searchMatches.value; - for (final previousMatch in previousMatches) { - if (previousMatch.node.diagnostic!.searchValue.caseInsensitiveContains( - search, - )) { - matches.add(previousMatch); - } - } - - if (matches.isNotEmpty) return matches; - } - - int debugStatsSearchOps = 0; - final debugStatsWidgets = _searchableCachedRows.length; - - final inspectorService = serviceConnection.inspectorService; - if (search.isEmpty || - inspectorService == null || - inspectorService.isDisposed) { - assert(() { - debugPrint('Search completed, no search'); - return true; - }()); - return matches; - } - - assert(() { - debugPrint('Search started: $_searchTarget'); - return true; - }()); - - for (final row in _searchableCachedRows) { - final diagnostic = row!.node.diagnostic; - if (diagnostic == null) continue; - - // Widget search begin - if (_searchTarget == SearchTargetType.widget) { - debugStatsSearchOps++; - if (diagnostic.searchValue.caseInsensitiveContains(search)) { - matches.add(row); - continue; - } - } - // Widget search end - } - - assert(() { - debugPrint( - 'Search completed with $debugStatsWidgets widgets, $debugStatsSearchOps ops', - ); - return true; - }()); - - return matches; - } -} - -extension RemoteDiagnosticsNodeExtension on RemoteDiagnosticsNode { - String get searchValue { - final description = toStringShort(); - final textPreview = json['textPreview']; - return textPreview is String - ? '$description ${textPreview.replaceAll('\n', ' ')}' - : description; - } -} - -abstract class InspectorControllerClient { - void onChanged(); - - void scrollToRect(Rect rect); - - void requestFocus(); -} - -class InspectorTree extends StatefulWidget { - const InspectorTree({ - super.key, - required this.controller, - required this.treeController, - this.summaryTreeController, - this.isSummaryTree = false, - this.widgetErrors, - this.screenId, - }) : assert(isSummaryTree == (summaryTreeController == null)); - - final InspectorController controller; - - final InspectorTreeController? treeController; - - /// Stores the summary tree controller when this instance of [InspectorTree] - /// is for the details tree (i.e. when [isSummaryTree] is false). - /// - /// This value should be null when this instance of [InspectorTree] is for the - /// summary tree itself. - final InspectorTreeController? summaryTreeController; - - final bool isSummaryTree; - final LinkedHashMap? widgetErrors; - final String? screenId; - - @override - State createState() => _InspectorTreeState(); -} - -// AutomaticKeepAlive is necessary so that the tree does not get recreated when we switch tabs. -class _InspectorTreeState extends State - with - SingleTickerProviderStateMixin, - AutomaticKeepAliveClientMixin, - AutoDisposeMixin - implements InspectorControllerClient { - InspectorController get controller => widget.controller; - InspectorTreeController? get treeController => widget.treeController; - - late ScrollController _scrollControllerY; - late ScrollController _scrollControllerX; - Future? _currentAnimateY; - Rect? _currentAnimateTarget; - - AnimationController? _constraintDisplayController; - late FocusNode _focusNode; - - /// When autoscrolling, the number of rows to pad the target location with. - static const _scrollPadCount = 3; - - @override - void initState() { - super.initState(); - _scrollControllerX = ScrollController(); - _scrollControllerY = ScrollController(); - // TODO(devoncarew): Commented out as per flutter/devtools/pull/2001. - //_scrollControllerY.addListener(_onScrollYChange); - if (widget.isSummaryTree) { - _constraintDisplayController = longAnimationController(this); - } - _focusNode = FocusNode(debugLabel: 'inspector-tree'); - autoDisposeFocusNode(_focusNode); - final mainIsolateState = - serviceConnection.serviceManager.isolateManager.mainIsolateState; - if (mainIsolateState != null) { - callOnceWhenReady( - trigger: mainIsolateState.isPaused, - callback: _bindToController, - readyWhen: (triggerValue) => !triggerValue, - ); - } - } - - @override - void didUpdateWidget(InspectorTree oldWidget) { - final oldTreeController = oldWidget.treeController; - if (oldTreeController != widget.treeController) { - oldTreeController?.removeClient(this); - - // TODO(elliette): Figure out if we can remove this. See explanation: - // https://github.com/flutter/devtools/pull/1290/files#r342399899. - cancelListeners(); - - _bindToController(); - } - super.didUpdateWidget(oldWidget); - } - - @override - void dispose() { - treeController?.removeClient(this); - _scrollControllerX.dispose(); - _scrollControllerY.dispose(); - _constraintDisplayController?.dispose(); - super.dispose(); - } - - @override - void requestFocus() { - _focusNode.requestFocus(); - } - - // TODO(devoncarew): Commented out as per flutter/devtools/pull/2001. - // void _onScrollYChange() { - // if (controller == null) return; - // - // // If the vertical position is already being animated we should not trigger - // // a new animation of the horizontal position as a more direct animation of - // // the horizontal position has already been triggered. - // if (currentAnimateY != null) return; - // - // final x = _computeTargetX(_scrollControllerY.offset); - // _scrollControllerX.animateTo( - // x, - // duration: defaultDuration, - // curve: defaultCurve, - // ); - // } - - @override - Future scrollToRect(Rect rect) async { - if (rect == _currentAnimateTarget) { - // We are in the middle of an animation to this exact rectangle. - return; - } - - final initialX = rect.left; - final initialY = rect.top; - final yOffsetAtViewportTop = _scrollControllerY.hasClients - ? _scrollControllerY.offset - : _scrollControllerY.initialScrollOffset; - final xOffsetAtViewportLeft = _scrollControllerX.hasClients - ? _scrollControllerX.offset - : _scrollControllerX.initialScrollOffset; - - final viewPortInScrollControllerSpace = Rect.fromLTWH( - xOffsetAtViewportLeft, - yOffsetAtViewportTop, - safeViewportWidth, - safeViewportHeight, - ); - - final isRectInViewPort = - viewPortInScrollControllerSpace.contains(rect.topLeft) && - viewPortInScrollControllerSpace.contains(rect.bottomRight); - if (isRectInViewPort) { - // The rect is already in view, don't scroll - return; - } - - _currentAnimateTarget = rect; - - final targetY = _padTargetY(initialY: initialY); - if (_scrollControllerY.hasClients) { - _currentAnimateY = _scrollControllerY.animateTo( - targetY, - duration: longDuration, - curve: defaultCurve, - ); - } else { - _currentAnimateY = null; - _scrollControllerY = ScrollController(initialScrollOffset: targetY); - } - - final targetX = _padTargetX(initialX: initialX); - if (_scrollControllerX.hasClients) { - unawaited( - _scrollControllerX.animateTo( - targetX, - duration: longDuration, - curve: defaultCurve, - ), - ); - } else { - _scrollControllerX = ScrollController(initialScrollOffset: targetX); - } - - try { - await _currentAnimateY; - } catch (e) { - // Doesn't matter if the animation was cancelled. - } - _currentAnimateY = null; - _currentAnimateTarget = null; - } - - // TODO(jacobr): resolve cases where we need to know the viewport height - // before it is available so we don't need this approximation. - /// Placeholder viewport height to use if we don't yet know the real - /// viewport height. - static const _placeholderViewportSize = Size(1000.0, 1000.0); - - double get safeViewportHeight { - return _scrollControllerY.hasClients - ? _scrollControllerY.position.viewportDimension - : _placeholderViewportSize.height; - } - - double get safeViewportWidth { - return _scrollControllerX.hasClients - ? _scrollControllerX.position.viewportDimension - : _placeholderViewportSize.width; - } - - /// Pad [initialX] with the horizontal indentation of [padCount] rows. - double _padTargetX({ - required double initialX, - int padCount = _scrollPadCount, - }) { - return initialX - inspectorColumnWidth * padCount; - } - - /// Pad [initialY] so that a row would be placed in the vertical center of - /// the screen. - double _padTargetY({required double initialY}) { - return initialY - (safeViewportHeight / 2) + inspectorRowHeight / 2; - } - - /// Handle arrow keys for the InspectorTree. Ignore other key events so that - /// other widgets have a chance to respond to them. - KeyEventResult _handleKeyEvent(FocusNode _, KeyEvent event) { - if (!event.isKeyDownOrRepeat) return KeyEventResult.ignored; - - final treeControllerLocal = treeController!; - - if (event.logicalKey == LogicalKeyboardKey.arrowDown) { - treeControllerLocal.navigateDown(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - treeControllerLocal.navigateUp(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - treeControllerLocal.navigateLeft(); - return KeyEventResult.handled; - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - treeControllerLocal.navigateRight(); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - } - - void _bindToController() { - treeController?.addClient(this); - } - - @override - void onChanged() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - super.build(context); - final treeControllerLocal = treeController; - if (treeControllerLocal == null) { - // Indicate the tree is loading. - return const CenteredCircularProgressIndicator(); - } - if (treeControllerLocal.numRows == 0) { - // This works around a bug when Scrollbars are present on a short lived - // widget. - return const SizedBox(); - } - - if (!controller.firstInspectorTreeLoadCompleted && widget.isSummaryTree) { - final screenId = widget.screenId; - if (screenId != null) { - ga.timeEnd( - screenId, - gac.pageReady, - screenMetricsProvider: () => InspectorScreenMetrics.legacy( - rowCount: treeControllerLocal.numRows, - ), - ); - unawaited( - serviceConnection.sendDwdsEvent( - screen: screenId, - action: gac.pageReady, - ), - ); - } - controller.firstInspectorTreeLoadCompleted = true; - } - return LayoutBuilder( - builder: (context, constraints) { - final viewportWidth = constraints.maxWidth; - final tree = Scrollbar( - thumbVisibility: true, - controller: _scrollControllerX, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - controller: _scrollControllerX, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: - treeControllerLocal.rowWidth + - treeControllerLocal.maxRowIndent, - ), - // TODO(kenz): this scrollbar needs to be sticky to the right side of - // the visible container - right now it is lined up to the right of - // the widest row (which is likely not visible). This may require some - // refactoring. - child: GestureDetector( - onTap: _focusNode.requestFocus, - child: Focus( - onKeyEvent: _handleKeyEvent, - autofocus: widget.isSummaryTree, - focusNode: _focusNode, - child: OffsetScrollbar( - isAlwaysShown: true, - axis: Axis.vertical, - controller: _scrollControllerY, - offsetController: _scrollControllerX, - offsetControllerViewportDimension: viewportWidth, - child: ListView.custom( - itemExtent: inspectorRowHeight, - childrenDelegate: SliverChildBuilderDelegate(( - context, - index, - ) { - if (index == treeControllerLocal.numRows) { - return const SizedBox(height: inspectorRowHeight); - } - final row = treeControllerLocal.getCachedRow(index)!; - final inspectorRef = row.node.diagnostic?.valueRef.id; - return _InspectorTreeRowWidget( - key: PageStorageKey(row.node), - inspectorTreeState: this, - row: row, - scrollControllerX: _scrollControllerX, - viewportWidth: viewportWidth, - error: - widget.widgetErrors != null && - inspectorRef != null - ? widget.widgetErrors![inspectorRef] - : null, - ); - }, childCount: treeControllerLocal.numRows + 1), - controller: _scrollControllerY, - ), - ), - ), - ), - ), - ), - ); - - final shouldShowBreadcrumbs = !widget.isSummaryTree; - if (shouldShowBreadcrumbs) { - final inspectorTreeController = widget.summaryTreeController!; - - final parents = inspectorTreeController - .getPathFromSelectedRowToRoot(); - return Column( - children: [ - InspectorBreadcrumbNavigator( - items: parents, - onTap: (node) => inspectorTreeController.onSelectNode(node), - ), - Expanded(child: tree), - ], - ); - } - - return tree; - }, - ); - } - - @override - bool get wantKeepAlive => true; -} - -Paint _defaultPaint(ColorScheme colorScheme) => Paint() - ..color = colorScheme.treeGuidelineColor - ..strokeWidth = chartLineStrokeWidth; - -/// Custom painter that draws lines indicating how parent and child rows are -/// connected to each other. -/// -/// Each rows object contains a list of ticks that indicate the x coordinates of -/// vertical lines connecting other rows need to be drawn within the vertical -/// area of the current row. This approach has the advantage that a row contains -/// all information required to render all content within it but has the -/// disadvantage that the x coordinates of each line connecting rows must be -/// computed in advance. -class _RowPainter extends CustomPainter { - _RowPainter(this.row, this._controller, this.colorScheme); - - final InspectorTreeController _controller; - final InspectorTreeRow row; - final ColorScheme colorScheme; - - @override - void paint(Canvas canvas, Size size) { - double currentX = 0; - final paint = _defaultPaint(colorScheme); - - final node = row.node; - final showExpandCollapse = node.showExpandCollapse; - for (final tick in row.ticks) { - currentX = _controller.getDepthIndent(tick) - inspectorColumnWidth * 0.5; - // Draw a vertical line for each tick identifying a connection between - // an ancestor of this node and some other node in the tree. - canvas.drawLine( - Offset(currentX, 0.0), - Offset(currentX, inspectorRowHeight), - paint, - ); - } - // If this row is itself connected to a parent then draw the L shaped line - // to make that connection. - if (row.lineToParent) { - currentX = - _controller.getDepthIndent(row.depth - 1) - - inspectorColumnWidth * 0.5; - final width = showExpandCollapse - ? inspectorColumnWidth * 0.5 - : inspectorColumnWidth; - canvas.drawLine( - Offset(currentX, 0.0), - Offset(currentX, inspectorRowHeight * 0.5), - paint, - ); - canvas.drawLine( - Offset(currentX, inspectorRowHeight * 0.5), - Offset(currentX + width, inspectorRowHeight * 0.5), - paint, - ); - } - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - if (oldDelegate is _RowPainter) { - // TODO(jacobr): check whether the row has different ticks. - return oldDelegate.colorScheme.isLight != colorScheme.isLight; - } - return true; - } -} - -/// Widget defining the contents of a single row in the InspectorTree. -/// -/// This class defines the scaffolding around the rendering of the actual -/// content of a [RemoteDiagnosticsNode] provided by -/// [DiagnosticsNodeDescription] to provide a tree implementation with lines -/// drawn between parent and child nodes when nodes have multiple children. -/// -/// Changes to how the actual content of the node within the row should -/// be implemented by changing [DiagnosticsNodeDescription] instead. -class InspectorRowContent extends StatelessWidget { - const InspectorRowContent({ - super.key, - required this.row, - required this.controller, - required this.onToggle, - required this.expandArrowAnimation, - this.error, - required this.scrollControllerX, - required this.viewportWidth, - }); - - final InspectorTreeRow row; - final InspectorTreeController controller; - final VoidCallback onToggle; - final Animation expandArrowAnimation; - final ScrollController scrollControllerX; - final double viewportWidth; - - /// A [DevToolsError] that applies to the widget in this row. - /// - /// This will be null if there is no error for this row. - final DevToolsError? error; - - /// Whether this row has any error. - bool get hasError => error != null; - - @override - Widget build(BuildContext context) { - final currentX = - controller.getDepthIndent(row.depth) - inspectorColumnWidth; - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - Color? backgroundColor; - if (row.isSelected) { - backgroundColor = hasError - ? colorScheme.errorContainer - : colorScheme.selectedRowBackgroundColor; - } - - final node = row.node; - - Widget rowWidget = Padding( - padding: EdgeInsets.only(left: currentX), - child: ValueListenableBuilder( - valueListenable: controller.searchNotifier, - builder: (context, searchValue, _) { - return Opacity( - opacity: searchValue.isEmpty || row.isSearchMatch ? 1 : 0.2, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - node.showExpandCollapse - ? InkWell( - onTap: onToggle, - child: RotationTransition( - turns: expandArrowAnimation, - child: const Icon( - Icons.expand_more, - size: defaultIconSize, - ), - ), - ) - : const SizedBox( - width: defaultSpacing, - height: defaultSpacing, - ), - Expanded( - child: Container( - color: backgroundColor, - child: InkWell( - onTap: () { - controller.onSelectRow(row); - // TODO(gmoothart): It may be possible to capture the tap - // and request focus directly from the InspectorTree. Then - // we wouldn't need this. - controller.requestFocus(); - }, - child: SizedBox( - height: inspectorRowHeight, - child: DiagnosticsNodeDescription( - node.diagnostic, - isSelected: row.isSelected, - searchValue: searchValue, - errorText: error?.errorMessage, - nodeDescriptionHighlightStyle: - searchValue.isEmpty || !row.isSearchMatch - ? DiagnosticsTextStyles.regular( - Theme.of(context).colorScheme, - ) - : row.isSelected - ? theme.searchMatchHighlightStyleFocused - : theme.searchMatchHighlightStyle, - ), - ), - ), - ), - ), - ], - ), - ); - }, - ), - ); - - // Wrap with tooltip if there is an error for this node's widget. - if (hasError) { - rowWidget = DevToolsTooltip( - message: error!.errorMessage, - child: rowWidget, - ); - } - - return CustomPaint( - painter: _RowPainter(row, controller, colorScheme), - size: Size(currentX, inspectorRowHeight), - child: Align( - alignment: Alignment.topLeft, - child: AnimatedBuilder( - animation: scrollControllerX, - builder: (context, child) { - final rowWidth = - scrollControllerX.offset + viewportWidth - defaultSpacing; - return SizedBox( - width: max(rowWidth, currentX + 100), - child: rowWidth > currentX ? child : const SizedBox(), - ); - }, - child: rowWidget, - ), - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart deleted file mode 100644 index ff579921994..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/box/box.dart +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; -import 'dart:math' as math; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/primitives/math_utils.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_data_models.dart'; -import '../ui/free_space.dart'; -import '../ui/layout_explorer_widget.dart'; -import '../ui/theme.dart'; -import '../ui/utils.dart'; -import '../ui/widget_constraints.dart'; -import '../ui/widgets_theme.dart'; - -/// Layout visualizer for a widget with a box-layout. -class BoxLayoutExplorerWidget extends LayoutExplorerWidget { - const BoxLayoutExplorerWidget(super.inspectorController, {super.key}); - - static bool shouldDisplay(RemoteDiagnosticsNode _) { - // Pretend this layout explorer is always available. This layout explorer - // will gracefully fall back to an error message if the required properties - // are not needed. - // TODO(jacobr) pass a RemoteDiagnosticsNode to this method that contains - // the layout explorer related supplemental properties so that we can - // accurately determine whether the widget uses box layout. - return true; - } - - @override - State createState() => - BoxLayoutExplorerWidgetState(); -} - -class BoxLayoutExplorerWidgetState - extends - LayoutExplorerWidgetState { - @override - RemoteDiagnosticsNode? getRoot(RemoteDiagnosticsNode? node) { - final nodeLocal = node; - if (nodeLocal == null) return null; - if (!shouldDisplay(nodeLocal)) return null; - return node; - } - - @override - bool shouldDisplay(RemoteDiagnosticsNode node) { - final selectedNodeLocal = selectedNode; - if (selectedNodeLocal == null) return false; - return BoxLayoutExplorerWidget.shouldDisplay(selectedNodeLocal); - } - - @override - AnimatedLayoutProperties computeAnimatedProperties( - LayoutProperties nextProperties, - ) { - return AnimatedLayoutProperties( - // If an animation is in progress, freeze it and start animating from there, else start a fresh animation from widget.properties. - animatedProperties?.copyWith() ?? properties!, - nextProperties, - changeAnimation, - ); - } - - @override - LayoutProperties computeLayoutProperties(RemoteDiagnosticsNode node) => - LayoutProperties(node); - - @override - void updateHighlighted(LayoutProperties? newProperties) { - setState(() { - // This implementation will need to change if we support showing more than - // a single widget in the box visualization for the layout explorer. - highlighted = newProperties != null && selectedNode == newProperties.node - ? newProperties - : null; - }); - } - - @override - Widget build(BuildContext context) { - if (properties == null) { - final selectedNodeLocal = selectedNode; - return Center( - child: Text( - '${selectedNodeLocal?.description ?? 'Widget'} has no layout properties to display.', - textAlign: TextAlign.center, - overflow: TextOverflow.clip, - ), - ); - } - return Container( - margin: const EdgeInsets.all(denseSpacing), - child: AnimatedBuilder( - animation: changeController, - builder: (context, _) { - return LayoutBuilder(builder: _buildLayout); - }, - ), - ); - } - - /// Simplistic layout algorithm to roughly match minFraction restrictions for - /// each sizes attempting to render a stylized version of the original layout. - /// TODO(jacobr): see if we can unify with the stylized version of the overall - /// layout used for Flex. Our constraints are quite different as we can - /// guarantee that the entire layout fits without scrolling while in the Flex - /// case that would be difficult. - /// - /// The overall layout will expand to use the full availableSize treating null - /// values in [sizes] as an indication that the items should have zero size. - /// On the other hand, a non-null size indicates that the minFractions - /// constraints should be obeyed. This is needed to ensure that negative sizes - /// are visualized reasonably. - /// The minFractions aren't exactly obeyed but they are approximated in a way - /// that keeps this algorithm simple and has the nice property that an initial - /// value much smaller than the minSize results in a slightly smaller value - /// than a value that is almost minSize. - /// In the most extreme case an item will get not minFraction but will instead - /// get the slightly smaller value of minFraction / (1 + minFraction) - /// which is close enough for the simple values we need this for. - static List minFractionLayout({ - required double availableSize, - required List sizes, - required List minFractions, - }) { - assert(sizes.length == minFractions.length); - final length = sizes.length; - double total = 1.0; // This isn't set to zero to avoid divide by zero bugs. - final fractions = minFractions.toList(); - for (final size in sizes) { - if (size != null) { - total += math.max(0, size); - } - } - - double totalFraction = 0.0; - for (int i = 0; i < length; i++) { - final size = sizes[i]; - if (size != null) { - fractions[i] = math.max(size / total, minFractions[i]); - totalFraction += fractions[i]; - } else { - fractions[i] = 0.0; - } - } - if (totalFraction != 1.0) { - for (int i = 0; i < length; i++) { - fractions[i] = fractions[i] / totalFraction; - } - } - final output = []; - for (final fraction in fractions) { - output.add(fraction * availableSize); - } - return output; - } - - Widget _buildChild(BuildContext context) { - final propertiesLocal = properties!; - - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final parentProperties = - this.parentProperties ?? - propertiesLocal; // Fall back to this node's properties if there is no parent. - - final parentSize = parentProperties.size; - final offset = propertiesLocal.node.parentData; - - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - // Subtract out one pixel border on each side. - final availableHeight = constraints.maxHeight - 2; - final availableWidth = constraints.maxWidth - 2; - - final minFractions = [0.2, 0.5, 0.2]; - // TODO(polinach, jacobr): consider using zeros for zero values, - // without replacing them with nulls. - // See https://github.com/flutter/devtools/issues/3931. - double? nullOutZero(double value) => value != 0.0 ? value : null; - final widths = [ - nullOutZero(offset.offset.dx), - propertiesLocal.size.width, - nullOutZero( - parentSize.width - (propertiesLocal.size.width + offset.offset.dx), - ), - ]; - final heights = [ - nullOutZero(offset.offset.dy), - propertiesLocal.size.height, - nullOutZero( - parentSize.height - - (propertiesLocal.size.height + offset.offset.dy), - ), - ]; - // 3 element array with [left padding, widget width, right padding]. - final displayWidths = minFractionLayout( - availableSize: availableWidth, - sizes: widths, - minFractions: minFractions, - ); - // 3 element array with [top padding, widget height, bottom padding]. - final displayHeights = minFractionLayout( - availableSize: availableHeight, - sizes: heights, - minFractions: minFractions, - ); - final widgetWidth = displayWidths[1]; - final widgetHeight = displayHeights[1]; - final safeParentSize = parentSize; - final width0 = widths[0]; - final width2 = widths[2]; - final height0 = heights[0]; - final height2 = heights[2]; - return Container( - width: constraints.maxWidth, - height: constraints.maxHeight, - decoration: BoxDecoration( - border: Border.all( - color: WidgetTheme.fromName( - propertiesLocal.node.description, - ).color, - ), - ), - child: Stack( - children: [ - LayoutExplorerBackground(colorScheme: colorScheme), - // Left padding. - if (width0 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayWidths[0], widgetHeight), - offset: Offset(0, displayHeights[0]), - realSize: Size(width0, safeParentSize.height), - layoutProperties: propertiesLocal, - isFreeSpace: true, - ), - horizontal: true, - ), - // Top padding. - if (height0 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(widgetWidth, displayHeights[0]), - offset: Offset(displayWidths[0], 0), - realSize: Size(safeParentSize.width, height0), - layoutProperties: propertiesLocal, - isFreeSpace: true, - ), - horizontal: false, - ), - // Right padding. - if (width2 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(displayWidths[2], widgetHeight), - offset: Offset( - displayWidths[0] + displayWidths[1], - displayHeights[0], - ), - realSize: Size(width2, safeParentSize.height), - layoutProperties: propertiesLocal, - isFreeSpace: true, - ), - horizontal: true, - ), - // Bottom padding. - if (height2 != null) - PaddingVisualizerWidget( - RenderProperties( - axis: Axis.horizontal, - size: Size(widgetWidth, displayHeights[2]), - offset: Offset( - displayWidths[0], - displayHeights[0] + displayHeights[1], - ), - realSize: Size(safeParentSize.width, height2), - layoutProperties: propertiesLocal, - isFreeSpace: true, - ), - horizontal: false, - ), - BoxChildVisualizer( - isSelected: true, - state: this, - layoutProperties: propertiesLocal, - renderProperties: RenderProperties( - axis: Axis.horizontal, - size: Size(widgetWidth, widgetHeight), - offset: Offset(displayWidths[0], displayHeights[0]), - realSize: propertiesLocal.size, - layoutProperties: propertiesLocal, - ), - ), - ], - ), - ); - }, - ); - } - - LayoutProperties? get parentProperties { - final parentElement = properties?.node.parentRenderElement; - if (parentElement == null) return null; - return computeLayoutProperties(parentElement); - } - - Widget _buildLayout(BuildContext context, BoxConstraints constraints) { - final maxHeight = constraints.maxHeight; - final maxWidth = constraints.maxWidth; - - Widget widget = _buildChild(context); - final parentProperties = this.parentProperties; - if (parentProperties != null) { - // Wrap with a widget visualizer for the parent if there is a valid parent. - widget = WidgetVisualizer( - // TODO(jacobr): this node's name can be misleading more often than - // in the flex case the widget doesn't have its own RenderObject. - // Consider showing the true ancestor for the summary tree that first - // has a different render object. - title: describeBoxName(parentProperties), - largeTitle: true, - layoutProperties: parentProperties, - isSelected: false, - child: VisualizeWidthAndHeightWithConstraints( - properties: parentProperties, - warnIfUnconstrained: false, - child: Padding( - padding: const EdgeInsets.all(denseSpacing), - child: widget, - ), - ), - ); - } - return Container( - constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight), - child: widget, - ); - } -} - -String describeBoxName(LayoutProperties properties) { - // Displaying a high quality name is more ambiguous for the Box case than the - // Flex case because the RenderObject for each widget is often quite - // different than the user expected as not all widgets have RenderObjects. - // As a compromise we currently show 'WidgetName - RenderObjectName'. - // This is clearer but risks more confusion - - // Widget name. - var title = properties.node.description ?? ''; - final renderDescription = properties.node.renderObject?.description; - // TODO(jacobr): consider de-emphasizing the render object name by putting it - // in more transparent text or just calling the widget Parent instead of - // surfacing a widget name. - if (renderDescription != null) { - // Name of the associated RenderObject if one is available. - title += ' - $renderDescription'; - } - return title; -} - -/// Widget that represents and visualize a direct child of Flex widget. -class BoxChildVisualizer extends StatelessWidget { - const BoxChildVisualizer({ - super.key, - required this.state, - required this.layoutProperties, - required this.renderProperties, - required this.isSelected, - }); - - final BoxLayoutExplorerWidgetState state; - - final bool isSelected; - final LayoutProperties layoutProperties; - final RenderProperties renderProperties; - - LayoutProperties? get properties => renderProperties.layoutProperties; - - @override - Widget build(BuildContext context) { - final renderSize = renderProperties.size; - final renderOffset = renderProperties.offset; - - Widget buildEntranceAnimation(BuildContext _, Widget? child) { - final size = renderSize; - // TODO(jacobr): does this entrance animation really add value. - return Opacity( - opacity: min([state.entranceCurve.value * 5, 1.0]), - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: math.max(0.0, (renderSize.width - size.width) / 2), - vertical: math.max(0.0, (renderSize.height - size.height) / 2), - ), - child: child, - ), - ); - } - - final propertiesLocal = properties!; - - return Positioned( - top: renderOffset.dy, - left: renderOffset.dx, - child: InkWell( - onTap: () => unawaited(state.onTap(propertiesLocal)), - onDoubleTap: () => state.onDoubleTap(propertiesLocal), - child: SizedBox( - width: safePositiveDouble(renderSize.width), - height: safePositiveDouble(renderSize.height), - child: AnimatedBuilder( - animation: state.entranceController, - builder: buildEntranceAnimation, - child: WidgetVisualizer( - isSelected: isSelected, - layoutProperties: layoutProperties, - title: describeBoxName(propertiesLocal), - // TODO(jacobr): consider surfacing the overflow size information - // if we determine - // overflowSide: properties.overflowSide, - - // We only show one child at a time so a large title is safe. - largeTitle: true, - child: VisualizeWidthAndHeightWithConstraints( - arrowHeadSize: arrowHeadSize, - properties: propertiesLocal, - warnIfUnconstrained: false, - child: const SizedBox.shrink(), - ), - ), - ), - ), - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart deleted file mode 100644 index c00d352397b..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/flex.dart +++ /dev/null @@ -1,777 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; -import 'dart:math' as math; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/diagnostics/inspector_service.dart'; -import '../../../../shared/primitives/math_utils.dart'; -import '../../../../shared/ui/common_widgets.dart'; -import '../../inspector_data_models.dart'; -import '../ui/arrow.dart'; -import '../ui/free_space.dart'; -import '../ui/layout_explorer_widget.dart'; -import '../ui/theme.dart'; -import '../ui/utils.dart'; -import '../ui/widget_constraints.dart'; -import 'utils.dart'; - -// TODO(kenz): clean up this file so that we use helper widgets instead of -// methods that pass around build context. - -// TODO(kenz): densify the layout explorer visualization for flex widgets. - -const alignmentDropdownMaxSize = 140.0; - -class FlexLayoutExplorerWidget extends LayoutExplorerWidget { - const FlexLayoutExplorerWidget(super.inspectorController, {super.key}); - - static bool shouldDisplay(RemoteDiagnosticsNode node) { - return (node.isFlex) || (node.parent?.isFlex ?? false); - } - - @override - State createState() => - FlexLayoutExplorerWidgetState(); -} - -class FlexLayoutExplorerWidgetState - extends - LayoutExplorerWidgetState< - FlexLayoutExplorerWidget, - FlexLayoutProperties - > { - final scrollController = ScrollController(); - - Axis get direction => properties!.direction; - - ObjectGroup? get objectGroup => - properties!.node.objectGroupApi as ObjectGroup?; - - Color horizontalColor(ColorScheme colorScheme) => - properties!.isMainAxisHorizontal - ? colorScheme.mainAxisColor - : colorScheme.crossAxisColor; - - Color verticalColor(ColorScheme colorScheme) => properties!.isMainAxisVertical - ? colorScheme.mainAxisColor - : colorScheme.crossAxisColor; - - Color horizontalTextColor(ColorScheme colorScheme) => - properties!.isMainAxisHorizontal - ? colorScheme.mainAxisTextColor - : colorScheme.crossAxisTextColor; - - Color verticalTextColor(ColorScheme colorScheme) => - properties!.isMainAxisVertical - ? colorScheme.mainAxisTextColor - : colorScheme.crossAxisTextColor; - - String get flexType => properties!.type; - - @override - RemoteDiagnosticsNode? getRoot(RemoteDiagnosticsNode? node) { - if (node == null) return null; - if (!shouldDisplay(node)) return null; - if (node.isFlex) return node; - return node.parent; - } - - @override - bool shouldDisplay(RemoteDiagnosticsNode node) { - final selectedNodeLocal = selectedNode; - if (selectedNodeLocal == null) return false; - return FlexLayoutExplorerWidget.shouldDisplay(selectedNodeLocal); - } - - @override - AnimatedFlexLayoutProperties computeAnimatedProperties( - FlexLayoutProperties nextProperties, - ) { - return AnimatedFlexLayoutProperties( - // If an animation is in progress, freeze it and start animating from there, else start a fresh animation from widget.properties. - animatedProperties?.copyWith() as FlexLayoutProperties? ?? properties!, - nextProperties, - changeAnimation, - ); - } - - @override - FlexLayoutProperties computeLayoutProperties(RemoteDiagnosticsNode node) => - FlexLayoutProperties.fromDiagnostics(node); - - @override - void updateHighlighted(FlexLayoutProperties? newProperties) { - setState(() { - if (selectedNode!.isFlex) { - highlighted = newProperties; - } else { - final idx = - selectedNode?.parent?.childrenNow.indexOf(selectedNode!) ?? -1; - if (newProperties == null) return; - if (idx != -1) highlighted = newProperties.children[idx]; - } - }); - } - - Widget _buildAxisAlignmentDropdown(Axis axis, ThemeData theme) { - final colorScheme = theme.colorScheme; - final color = axis == direction - ? colorScheme.mainAxisTextColor - : colorScheme.crossAxisTextColor; - List alignmentEnumEntries; - Object? selected; - final propertiesLocal = properties!; - if (axis == direction) { - alignmentEnumEntries = MainAxisAlignment.values; - selected = propertiesLocal.mainAxisAlignment; - } else { - alignmentEnumEntries = CrossAxisAlignment.values.toList(growable: true); - if (propertiesLocal.textBaseline == null) { - // TODO(albertusangga): Look for ways to visualize baseline when it is null - alignmentEnumEntries.remove(CrossAxisAlignment.baseline); - } - selected = propertiesLocal.crossAxisAlignment; - } - return RotatedBox( - quarterTurns: axis == Axis.vertical ? 3 : 0, - child: Container( - constraints: const BoxConstraints( - maxWidth: alignmentDropdownMaxSize, - maxHeight: defaultButtonHeight, - ), - child: DropdownButton( - value: selected, - isDense: true, - isExpanded: true, - // Avoid showing an underline for the main axis and cross-axis drop downs. - underline: const SizedBox(), - iconEnabledColor: axis == propertiesLocal.direction - ? colorScheme.mainAxisColor - : colorScheme.crossAxisColor, - selectedItemBuilder: (context) { - return [ - for (final alignment in alignmentEnumEntries) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Text( - alignment.name, - style: theme.regularTextStyleWithColor(color), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - height: actionsIconSize, - width: actionsIconSize, - child: Image.asset( - (axis == direction) - ? mainAxisAssetImageUrl( - direction, - alignment as MainAxisAlignment, - ) - : crossAxisAssetImageUrl( - direction, - alignment as CrossAxisAlignment, - ), - height: axisAlignmentAssetImageHeight, - fit: BoxFit.fitHeight, - color: color, - ), - ), - ], - ), - ]; - }, - items: [ - for (final alignment in alignmentEnumEntries) - DropdownMenuItem( - value: alignment, - child: Container( - padding: const EdgeInsets.symmetric(vertical: margin), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - child: Text( - alignment.name, - style: theme.regularTextStyleWithColor(color), - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - ), - ), - SizedBox( - height: actionsIconSize, - width: actionsIconSize, - child: Image.asset( - (axis == direction) - ? mainAxisAssetImageUrl( - direction, - alignment as MainAxisAlignment, - ) - : crossAxisAssetImageUrl( - direction, - alignment as CrossAxisAlignment, - ), - fit: BoxFit.fitHeight, - color: color, - ), - ), - ], - ), - ), - ), - ], - onChanged: (Object? newSelection) async { - // newSelection is an object instead of type here because - // the type is dependent on the `axis` parameter - // if the axis is the main axis the type should be [MainAxisAlignment] - // if the axis is the cross axis the type should be [CrossAxisAlignment] - FlexLayoutProperties changedProperties; - changedProperties = axis == direction - ? propertiesLocal.copyWith( - mainAxisAlignment: newSelection as MainAxisAlignment?, - ) - : propertiesLocal.copyWith( - crossAxisAlignment: newSelection as CrossAxisAlignment?, - ); - final valueRef = propertiesLocal.node.valueRef; - markAsDirty(); - await objectGroup!.invokeSetFlexProperties( - valueRef, - changedProperties.mainAxisAlignment, - changedProperties.crossAxisAlignment, - ); - }, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - if (properties == null) return const SizedBox(); - return Container( - margin: const EdgeInsets.all(margin), - padding: const EdgeInsets.only(bottom: margin, right: margin), - child: AnimatedBuilder( - animation: changeController, - builder: (context, _) { - return LayoutBuilder(builder: _buildLayout); - }, - ), - ); - } - - Widget _buildLayout(BuildContext context, BoxConstraints constraints) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final maxHeight = constraints.maxHeight; - final maxWidth = constraints.maxWidth; - final propertiesLocal = properties!; - final flexDescription = Align( - alignment: Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.only( - top: mainAxisArrowIndicatorSize, - left: crossAxisArrowIndicatorSize + margin, - ), - child: InkWell( - onTap: () => unawaited(onTap(propertiesLocal)), - child: WidgetVisualizer( - title: flexType, - layoutProperties: propertiesLocal, - isSelected: highlighted == properties, - overflowSide: propertiesLocal.overflowSide, - hint: Container( - padding: const EdgeInsets.all(4.0), - child: Text( - 'Total Flex Factor: ${propertiesLocal.totalFlex.toInt()}', - style: theme.regularTextStyleWithColor(emphasizedTextColor), - overflow: TextOverflow.ellipsis, - ), - ), - child: VisualizeFlexChildren( - state: this, - properties: propertiesLocal, - children: children, - highlighted: highlighted, - scrollController: scrollController, - direction: direction, - ), - ), - ), - ), - ); - - final verticalAxisDescription = Align( - alignment: Alignment.bottomLeft, - child: Container( - margin: const EdgeInsets.only(top: mainAxisArrowIndicatorSize + margin), - width: crossAxisArrowIndicatorSize, - child: Column( - children: [ - Expanded( - child: ArrowWrapper.unidirectional( - arrowColor: verticalColor(colorScheme), - type: ArrowType.down, - child: Truncateable( - truncate: maxHeight <= minHeightToAllowTruncating, - child: RotatedBox( - quarterTurns: 3, - child: Text( - propertiesLocal.verticalDirectionDescription, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.regularTextStyleWithColor( - verticalTextColor(colorScheme), - ), - ), - ), - ), - ), - ), - Truncateable( - truncate: maxHeight <= minHeightToAllowTruncating, - child: _buildAxisAlignmentDropdown(Axis.vertical, theme), - ), - ], - ), - ), - ); - - final horizontalAxisDescription = Align( - alignment: Alignment.topRight, - child: Container( - margin: const EdgeInsets.only( - left: crossAxisArrowIndicatorSize + margin, - ), - height: mainAxisArrowIndicatorSize, - child: Row( - children: [ - Expanded( - child: ArrowWrapper.unidirectional( - arrowColor: horizontalColor(colorScheme), - type: ArrowType.right, - child: Truncateable( - truncate: maxWidth <= minWidthToAllowTruncating, - child: Text( - propertiesLocal.horizontalDirectionDescription, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.center, - style: theme.regularTextStyleWithColor( - horizontalTextColor(colorScheme), - ), - ), - ), - ), - ), - Truncateable( - truncate: maxWidth <= minWidthToAllowTruncating, - child: _buildAxisAlignmentDropdown(Axis.horizontal, theme), - ), - ], - ), - ), - ); - - return Container( - constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight), - child: Stack( - children: [ - flexDescription, - verticalAxisDescription, - horizontalAxisDescription, - ], - ), - ); - } -} - -class VisualizeFlexChildren extends StatefulWidget { - const VisualizeFlexChildren({ - super.key, - required this.state, - required this.properties, - required this.children, - required this.highlighted, - required this.scrollController, - required this.direction, - }); - - final FlexLayoutProperties properties; - final List children; - final LayoutProperties? highlighted; - final ScrollController scrollController; - final Axis direction; - final FlexLayoutExplorerWidgetState state; - - @override - State createState() => _VisualizeFlexChildrenState(); -} - -class _VisualizeFlexChildrenState extends State { - LayoutProperties? lastHighlighted; - static final selectedChildKey = GlobalKey(debugLabel: 'selectedChild'); - - @override - Widget build(BuildContext context) { - if (lastHighlighted != widget.highlighted) { - lastHighlighted = widget.highlighted; - if (widget.highlighted != null) { - WidgetsBinding.instance.addPostFrameCallback((_) { - final selectedRenderObject = selectedChildKey.currentContext - ?.findRenderObject(); - if (selectedRenderObject != null && - widget.scrollController.hasClients) { - unawaited( - widget.scrollController.position.ensureVisible( - selectedRenderObject, - alignment: 0.5, - duration: defaultDuration, - ), - ); - } - }); - } - } - - if (!widget.properties.hasChildren) { - return const CenteredMessage(message: 'No Children'); - } - - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - - final contents = Container( - decoration: BoxDecoration( - border: Border.all(color: theme.primaryColorLight), - ), - margin: const EdgeInsets.only(top: margin, left: margin), - child: LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final maxHeight = constraints.maxHeight; - - double maxSizeAvailable(Axis axis) { - return axis == Axis.horizontal ? maxWidth : maxHeight; - } - - final childrenAndMainAxisSpacesRenderProps = widget.properties - .childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - - final renderProperties = childrenAndMainAxisSpacesRenderProps - .where((renderProps) => !renderProps.isFreeSpace) - .toList(); - final mainAxisSpaces = childrenAndMainAxisSpacesRenderProps - .where((renderProps) => renderProps.isFreeSpace) - .toList(); - final crossAxisSpaces = widget.properties.crossAxisSpaces( - childrenRenderProperties: renderProperties, - maxSizeAvailable: maxSizeAvailable, - ); - - final childrenRenderWidgets = []; - Widget? selectedWidget; - for (var i = 0; i < widget.children.length; i++) { - final child = widget.children[i]; - final isSelected = widget.highlighted == child; - - final visualizer = FlexChildVisualizer( - key: isSelected ? selectedChildKey : null, - state: widget.state, - layoutProperties: child, - isSelected: isSelected, - renderProperties: renderProperties[i], - ); - - if (isSelected) { - selectedWidget = visualizer; - } else { - childrenRenderWidgets.add(visualizer); - } - } - - // Selected widget needs to be last to draw its border over other children - if (selectedWidget != null) { - childrenRenderWidgets.add(selectedWidget); - } - - final freeSpacesWidgets = [ - for (final renderProperties in [ - ...mainAxisSpaces, - ...crossAxisSpaces, - ]) - FreeSpaceVisualizerWidget(renderProperties), - ]; - return Scrollbar( - thumbVisibility: true, - controller: widget.scrollController, - child: SingleChildScrollView( - scrollDirection: widget.properties.direction, - controller: widget.scrollController, - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: maxWidth, - minHeight: maxHeight, - maxWidth: widget.direction == Axis.horizontal - ? sum( - childrenAndMainAxisSpacesRenderProps.map( - (renderSize) => renderSize.width, - ), - ) - : maxWidth, - maxHeight: widget.direction == Axis.vertical - ? sum( - childrenAndMainAxisSpacesRenderProps.map( - (renderSize) => renderSize.height, - ), - ) - : maxHeight, - ).normalize(), - child: Stack( - children: [ - LayoutExplorerBackground(colorScheme: colorScheme), - ...freeSpacesWidgets, - ...childrenRenderWidgets, - ], - ), - ), - ), - ); - }, - ), - ); - return VisualizeWidthAndHeightWithConstraints( - properties: widget.properties, - child: contents, - ); - } -} - -/// Widget that represents and visualize a direct child of Flex widget. -class FlexChildVisualizer extends StatelessWidget { - const FlexChildVisualizer({ - super.key, - required this.state, - required this.layoutProperties, - required this.renderProperties, - required this.isSelected, - }); - - final FlexLayoutExplorerWidgetState state; - - final bool isSelected; - - final LayoutProperties layoutProperties; - - final RenderProperties renderProperties; - - // TODO(polina-c, jacob314): consider refactoring to remove `!`. - FlexLayoutProperties get root => state.properties!; - - LayoutProperties get properties => renderProperties.layoutProperties; - - ObjectGroup? get objectGroup => - properties.node.objectGroupApi as ObjectGroup?; - - void onChangeFlexFactor(int? newFlexFactor) async { - state.markAsDirty(); - await objectGroup!.invokeSetFlexFactor( - properties.node.valueRef, - newFlexFactor, - ); - } - - void onChangeFlexFit(FlexFit? newFlexFit) async { - state.markAsDirty(); - await objectGroup!.invokeSetFlexFit(properties.node.valueRef, newFlexFit!); - } - - Widget _buildFlexFactorChangerDropdown( - int maximumFlexFactor, - ThemeData theme, - ) { - final propertiesLocal = properties; - - Widget buildMenuitemChild(int? flexFactor) { - return Text( - 'flex: $flexFactor', - style: flexFactor == propertiesLocal.flexFactor - ? theme.boldTextStyle.copyWith(color: emphasizedTextColor) - : theme.regularTextStyleWithColor(emphasizedTextColor), - ); - } - - DropdownMenuItem buildMenuItem(int? flexFactor) { - return DropdownMenuItem( - value: flexFactor, - child: buildMenuitemChild(flexFactor), - ); - } - - return DropdownButton( - value: propertiesLocal.flexFactor?.toInt().clamp(0, maximumFlexFactor), - onChanged: onChangeFlexFactor, - iconEnabledColor: textColor, - underline: buildUnderline(), - items: >[ - buildMenuItem(null), - for (var i = 0; i <= maximumFlexFactor; ++i) buildMenuItem(i), - ], - ); - } - - Widget _buildFlexFitChangerDropdown(ThemeData theme) { - Widget flexFitDescription(FlexFit flexFit) => Text( - 'fit: ${flexFit.name}', - style: theme.regularTextStyleWithColor(emphasizedTextColor), - ); - - final propertiesLocal = properties; - - // Disable FlexFit changer if widget is Expanded. - if (propertiesLocal.description == 'Expanded') { - return flexFitDescription(FlexFit.tight); - } - - DropdownMenuItem buildMenuItem(FlexFit flexFit) { - return DropdownMenuItem( - value: flexFit, - child: flexFitDescription(flexFit), - ); - } - - return DropdownButton( - value: propertiesLocal.flexFit, - onChanged: onChangeFlexFit, - underline: buildUnderline(), - iconEnabledColor: emphasizedTextColor, - items: >[ - buildMenuItem(FlexFit.loose), - if (propertiesLocal.description != 'Expanded') - buildMenuItem(FlexFit.tight), - ], - ); - } - - Widget _buildContent(ThemeData theme) { - // TODO(https://github.com/flutter/devtools/issues/4058) allow more dynamic - // flex factor input - final currentFlexFactor = properties.flexFactor?.toInt() ?? 0; - final currentMaxFlexFactor = math.max( - currentFlexFactor, - maximumFlexFactorOptions, - ); - - return Container( - margin: const EdgeInsets.only(top: margin, left: margin), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Flexible( - child: _buildFlexFactorChangerDropdown(currentMaxFlexFactor, theme), - ), - if (!properties.hasFlexFactor) - Text( - 'unconstrained ${root.isMainAxisHorizontal ? 'horizontal' : 'vertical'}', - style: theme.regularTextStyle.copyWith( - color: theme.colorScheme.unconstrainedColor, - fontStyle: FontStyle.italic, - ), - maxLines: 2, - softWrap: true, - overflow: TextOverflow.ellipsis, - textScaler: const TextScaler.linear(smallTextScaleFactor), - textAlign: TextAlign.center, - ), - _buildFlexFitChangerDropdown(theme), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - final renderSize = renderProperties.size; - final renderOffset = renderProperties.offset; - final propertiesLocal = properties; - final rootLocal = root; - - Widget buildEntranceAnimation(BuildContext _, Widget? child) { - final vertical = rootLocal.isMainAxisVertical; - final horizontal = rootLocal.isMainAxisHorizontal; - - late Size size; - size = propertiesLocal.hasFlexFactor - ? SizeTween( - begin: Size( - horizontal ? minRenderWidth - entranceMargin : renderSize.width, - vertical ? minRenderHeight - entranceMargin : renderSize.height, - ), - end: renderSize, - ).evaluate(state.entranceCurve)! - : renderSize; - // Not-expanded widgets enter much faster. - return Opacity( - opacity: min([state.entranceCurve.value * 5, 1.0]), - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: math.max(0.0, (renderSize.width - size.width) / 2), - vertical: math.max(0.0, (renderSize.height - size.height) / 2), - ), - child: child, - ), - ); - } - - return Positioned( - top: renderOffset.dy, - left: renderOffset.dx, - child: GestureDetector( - onTap: () => unawaited(state.onTap(propertiesLocal)), - onDoubleTap: () => state.onDoubleTap(propertiesLocal), - onLongPress: () => state.onDoubleTap(propertiesLocal), - child: SizedBox( - width: renderSize.width, - height: renderSize.height, - child: AnimatedBuilder( - animation: state.entranceController, - builder: buildEntranceAnimation, - child: WidgetVisualizer( - isSelected: isSelected, - layoutProperties: layoutProperties, - title: propertiesLocal.description ?? '', - overflowSide: propertiesLocal.overflowSide, - child: VisualizeWidthAndHeightWithConstraints( - arrowHeadSize: arrowHeadSize, - properties: propertiesLocal, - child: Align( - alignment: Alignment.topRight, - child: _buildContent(Theme.of(context)), - ), - ), - ), - ), - ), - ), - ); - } - - /// define the number of flex factor to be shown in the flex dropdown button - /// for example if it's set to 5 the dropdown will consist of 6 items (null and 0..5) - static const maximumFlexFactorOptions = 5; -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart deleted file mode 100644 index cc3b2019ca3..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/flex/utils.dart +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:ui'; - -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../inspector_data_models.dart'; -import '../ui/utils.dart'; - -String crossAxisAssetImageUrl(Axis direction, CrossAxisAlignment alignment) { - return 'assets/img/layout_explorer/cross_axis_alignment/' - '${direction.flexType.toLowerCase()}_${alignment.name}.png'; -} - -String mainAxisAssetImageUrl(Axis direction, MainAxisAlignment alignment) { - return 'assets/img/layout_explorer/main_axis_alignment/' - '${direction.flexType.toLowerCase()}_${alignment.name}.png'; -} - -class AnimatedFlexLayoutProperties - extends AnimatedLayoutProperties - implements FlexLayoutProperties { - AnimatedFlexLayoutProperties(super.begin, super.end, super.animation); - - @override - CrossAxisAlignment? get crossAxisAlignment => end.crossAxisAlignment; - - @override - MainAxisAlignment? get mainAxisAlignment => end.mainAxisAlignment; - - @override - List childrenRenderProperties({ - required double smallestRenderWidth, - required double largestRenderWidth, - required double smallestRenderHeight, - required double largestRenderHeight, - required double Function(Axis) maxSizeAvailable, - }) { - final beginRenderProperties = begin.childrenRenderProperties( - smallestRenderHeight: smallestRenderHeight, - smallestRenderWidth: smallestRenderWidth, - largestRenderHeight: largestRenderHeight, - largestRenderWidth: largestRenderWidth, - maxSizeAvailable: maxSizeAvailable, - ); - final endRenderProperties = end.childrenRenderProperties( - smallestRenderHeight: smallestRenderHeight, - smallestRenderWidth: smallestRenderWidth, - largestRenderHeight: largestRenderHeight, - largestRenderWidth: largestRenderWidth, - maxSizeAvailable: maxSizeAvailable, - ); - final result = []; - for (var i = 0; i < children.length; i++) { - final beginProps = beginRenderProperties[i]; - final endProps = endRenderProperties[i]; - final t = animation.value; - result.add( - RenderProperties( - axis: endProps.axis, - offset: Offset.lerp(beginProps.offset, endProps.offset, t), - size: Size.lerp(beginProps.size, endProps.size, t), - realSize: Size.lerp(beginProps.realSize, endProps.realSize, t), - layoutProperties: AnimatedLayoutProperties( - beginProps.layoutProperties, - endProps.layoutProperties, - animation, - ), - ), - ); - } - // Add in the free space from the end. - // TODO(djshuckerow): We should make free space a part of - // RenderProperties so that we can animate between those. - result.addAll(endRenderProperties.where((prop) => prop.isFreeSpace)); - return result; - } - - @override - double get crossAxisDimension => lerpDouble( - begin.crossAxisDimension, - end.crossAxisDimension, - animation.value, - )!; - - @override - Axis get crossAxisDirection => end.crossAxisDirection; - - @override - List crossAxisSpaces({ - required List childrenRenderProperties, - required double Function(Axis) maxSizeAvailable, - }) { - return end.crossAxisSpaces( - childrenRenderProperties: childrenRenderProperties, - maxSizeAvailable: maxSizeAvailable, - ); - } - - @override - Axis get direction => end.direction; - - @override - String get horizontalDirectionDescription => - end.horizontalDirectionDescription; - - @override - bool get isMainAxisHorizontal => end.isMainAxisHorizontal; - - @override - bool get isMainAxisVertical => end.isMainAxisVertical; - - @override - double get mainAxisDimension => lerpDouble( - begin.mainAxisDimension, - end.mainAxisDimension, - animation.value, - )!; - - @override - MainAxisSize? get mainAxisSize => end.mainAxisSize; - - @override - TextBaseline? get textBaseline => end.textBaseline; - - @override - TextDirection get textDirection => end.textDirection; - - @override - double get totalFlex => - lerpDouble(begin.totalFlex, end.totalFlex, animation.value)!; - - @override - String get type => end.type; - - @override - VerticalDirection get verticalDirection => end.verticalDirection; - - @override - String get verticalDirectionDescription => end.verticalDirectionDescription; - - /// Returns a frozen copy of these FlexLayoutProperties that does not animate. - /// - /// Useful for interrupting an animation with a transition to another [FlexLayoutProperties]. - @override - FlexLayoutProperties copyWith({ - Size? size, - List? children, - BoxConstraints? constraints, - bool? isFlex, - String? description, - num? flexFactor, - FlexFit? flexFit, - Axis? direction, - MainAxisAlignment? mainAxisAlignment, - MainAxisSize? mainAxisSize, - CrossAxisAlignment? crossAxisAlignment, - TextDirection? textDirection, - VerticalDirection? verticalDirection, - TextBaseline? textBaseline, - }) { - return FlexLayoutProperties( - size: size ?? this.size, - children: children ?? this.children, - node: node, - constraints: constraints ?? this.constraints, - isFlex: isFlex ?? this.isFlex, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - direction: direction ?? this.direction, - mainAxisAlignment: mainAxisAlignment ?? this.mainAxisAlignment, - mainAxisSize: mainAxisSize ?? this.mainAxisSize, - crossAxisAlignment: crossAxisAlignment ?? this.crossAxisAlignment, - textDirection: textDirection ?? this.textDirection, - verticalDirection: verticalDirection ?? this.verticalDirection, - textBaseline: textBaseline ?? this.textBaseline, - ); - } - - @override - bool get startIsTopLeft => end.startIsTopLeft; -} - -/// LayoutProperties extension to be reused on LayoutProperties and AnimatedLayoutProperties - -extension AxisExtension on Axis { - String get flexType { - switch (this) { - case Axis.horizontal: - return 'Row'; - case Axis.vertical: - return 'Column'; - } - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart deleted file mode 100644 index 116a3c6c849..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/layout_explorer.dart +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../shared/diagnostics/diagnostics_node.dart'; -import '../inspector_controller.dart'; -import '../layout_explorer/box/box.dart'; -import '../layout_explorer/flex/flex.dart'; - -/// Tab that acts as a proxy to decide which widget to be displayed -class LayoutExplorerTab extends StatefulWidget { - const LayoutExplorerTab({super.key, required this.controller}); - - final InspectorController controller; - - @override - State createState() => _LayoutExplorerTabState(); -} - -class _LayoutExplorerTabState extends State - with AutomaticKeepAliveClientMixin, AutoDisposeMixin { - InspectorController get controller => widget.controller; - - RemoteDiagnosticsNode? get selected => - controller.selectedNode.value?.diagnostic; - - RemoteDiagnosticsNode? previousSelection; - - Widget rootWidget(RemoteDiagnosticsNode? node) { - if (node != null && FlexLayoutExplorerWidget.shouldDisplay(node)) { - return FlexLayoutExplorerWidget(controller); - } - if (node != null && BoxLayoutExplorerWidget.shouldDisplay(node)) { - return BoxLayoutExplorerWidget(controller); - } - return Center( - child: Text( - node != null - ? 'Currently, Layout Explorer only supports Box and Flex-based widgets.' - : 'Select a widget to view its layout.', - textAlign: TextAlign.center, - overflow: TextOverflow.clip, - ), - ); - } - - void onSelectionChanged() { - if (rootWidget(previousSelection).runtimeType != - rootWidget(selected).runtimeType) { - setState(() => previousSelection = selected); - } - } - - @override - void initState() { - super.initState(); - addAutoDisposeListener(controller.selectedNode, onSelectionChanged); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return rootWidget(selected); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart deleted file mode 100644 index 7fcf002900a..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/arrow.dart +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:math'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -const defaultArrowColor = Colors.white; -const defaultArrowStrokeWidth = 2.0; -const defaultDistanceToArrow = 4.0; - -enum ArrowType { up, left, right, down } - -Axis axis(ArrowType type) => (type == ArrowType.up || type == ArrowType.down) - ? Axis.vertical - : Axis.horizontal; - -/// Widget that draws a bidirectional arrow around another widget. -/// -/// This widget is typically used to help draw diagrams. -@immutable -class ArrowWrapper extends StatelessWidget { - ArrowWrapper.unidirectional({ - super.key, - this.child, - required ArrowType type, - this.arrowColor = defaultArrowColor, - double? arrowHeadSize, - this.arrowStrokeWidth = defaultArrowStrokeWidth, - this.childMarginFromArrow = defaultDistanceToArrow, - }) : assert(childMarginFromArrow > 0.0), - direction = axis(type), - isBidirectional = false, - startArrowType = type, - endArrowType = type, - arrowHeadSize = arrowHeadSize ?? defaultIconSize; - - const ArrowWrapper.bidirectional({ - super.key, - this.child, - required this.direction, - this.arrowColor = defaultArrowColor, - required this.arrowHeadSize, - this.arrowStrokeWidth = defaultArrowStrokeWidth, - this.childMarginFromArrow = defaultDistanceToArrow, - }) : assert(arrowHeadSize >= 0.0), - assert(childMarginFromArrow >= 0.0), - isBidirectional = true, - startArrowType = direction == Axis.horizontal - ? ArrowType.left - : ArrowType.up, - endArrowType = direction == Axis.horizontal - ? ArrowType.right - : ArrowType.down; - - final Color arrowColor; - final double arrowHeadSize; - final double arrowStrokeWidth; - final Widget? child; - - final Axis direction; - final double childMarginFromArrow; - - final bool isBidirectional; - final ArrowType startArrowType; - final ArrowType endArrowType; - - double get verticalMarginFromArrow { - if (child == null || direction == Axis.horizontal) return 0.0; - return childMarginFromArrow; - } - - double get horizontalMarginFromArrow { - if (child == null || direction == Axis.vertical) return 0.0; - return childMarginFromArrow; - } - - @override - Widget build(BuildContext context) { - return Flex( - direction: direction, - children: [ - Expanded( - child: Container( - margin: EdgeInsets.only( - bottom: verticalMarginFromArrow, - right: horizontalMarginFromArrow, - ), - child: ArrowWidget( - color: arrowColor, - headSize: arrowHeadSize, - strokeWidth: arrowStrokeWidth, - type: startArrowType, - shouldDrawHead: isBidirectional - ? true - : (startArrowType == ArrowType.left || - startArrowType == ArrowType.up), - ), - ), - ), - if (child != null) child!, - Expanded( - child: Container( - margin: EdgeInsets.only( - top: verticalMarginFromArrow, - left: horizontalMarginFromArrow, - ), - child: ArrowWidget( - color: arrowColor, - headSize: arrowHeadSize, - strokeWidth: arrowStrokeWidth, - type: endArrowType, - shouldDrawHead: isBidirectional - ? true - : (endArrowType == ArrowType.right || - endArrowType == ArrowType.down), - ), - ), - ), - ], - ); - } -} - -/// Widget that draws a fully sized, centered, unidirectional arrow according to its constraints -@immutable -class ArrowWidget extends StatelessWidget { - ArrowWidget({ - this.color = defaultArrowColor, - required this.headSize, - super.key, - this.shouldDrawHead = true, - this.strokeWidth = defaultArrowStrokeWidth, - required this.type, - }) : assert(headSize > 0.0), - assert(strokeWidth > 0.0), - _painter = _ArrowPainter( - headSize: headSize, - color: color, - strokeWidth: strokeWidth, - type: type, - shouldDrawHead: shouldDrawHead, - ); - - final Color color; - - /// The arrow head is a Equilateral triangle - final double headSize; - - final double strokeWidth; - - final ArrowType type; - - final CustomPainter _painter; - - final bool shouldDrawHead; - - @override - Widget build(BuildContext context) { - return CustomPaint(painter: _painter, child: Container()); - } -} - -class _ArrowPainter extends CustomPainter { - _ArrowPainter({ - required this.headSize, - this.strokeWidth = defaultArrowStrokeWidth, - this.color = defaultArrowColor, - this.shouldDrawHead = true, - required this.type, - }) : // the height of an equilateral triangle - headHeight = 0.5 * sqrt(3) * headSize; - - final Color color; - final double headSize; - final bool shouldDrawHead; - final double strokeWidth; - final ArrowType type; - - final double headHeight; - - bool headIsGreaterThanConstraint(Size size) { - if (type == ArrowType.left || type == ArrowType.right) { - return headHeight >= (size.width); - } - return headHeight >= (size.height); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) => - !(oldDelegate is _ArrowPainter && - headSize == oldDelegate.headSize && - strokeWidth == oldDelegate.strokeWidth && - color == oldDelegate.color && - type == oldDelegate.type); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..strokeWidth = strokeWidth; - - final originX = size.width / 2, originY = size.height / 2; - Offset lineStartingPoint = Offset.zero; - Offset lineEndingPoint = Offset.zero; - - if (!headIsGreaterThanConstraint(size) && shouldDrawHead) { - Offset p1, p2, p3; - final headSizeDividedBy2 = headSize / 2; - switch (type) { - case ArrowType.up: - p1 = Offset(originX, 0); - p2 = Offset(originX - headSizeDividedBy2, headHeight); - p3 = Offset(originX + headSizeDividedBy2, headHeight); - break; - case ArrowType.left: - p1 = Offset(0, originY); - p2 = Offset(headHeight, originY - headSizeDividedBy2); - p3 = Offset(headHeight, originY + headSizeDividedBy2); - break; - case ArrowType.right: - final startingX = size.width - headHeight; - p1 = Offset(size.width, originY); - p2 = Offset(startingX, originY - headSizeDividedBy2); - p3 = Offset(startingX, originY + headSizeDividedBy2); - break; - case ArrowType.down: - final startingY = size.height - headHeight; - p1 = Offset(originX, size.height); - p2 = Offset(originX - headSizeDividedBy2, startingY); - p3 = Offset(originX + headSizeDividedBy2, startingY); - break; - } - final path = Path() - ..moveTo(p1.dx, p1.dy) - ..lineTo(p2.dx, p2.dy) - ..lineTo(p3.dx, p3.dy) - ..close(); - canvas.drawPath(path, paint); - - switch (type) { - case ArrowType.up: - lineStartingPoint = Offset(originX, headHeight); - lineEndingPoint = Offset(originX, size.height); - break; - case ArrowType.left: - lineStartingPoint = Offset(headHeight, originY); - lineEndingPoint = Offset(size.width, originY); - break; - case ArrowType.right: - final arrowHeadStartingX = size.width - headHeight; - lineStartingPoint = Offset(0, originY); - lineEndingPoint = Offset(arrowHeadStartingX, originY); - break; - case ArrowType.down: - final headStartingY = size.height - headHeight; - lineStartingPoint = Offset(originX, 0); - lineEndingPoint = Offset(originX, headStartingY); - break; - } - } else { - // draw full line - switch (type) { - case ArrowType.up: - case ArrowType.down: - lineStartingPoint = Offset(originX, 0); - lineEndingPoint = Offset(originX, size.height); - break; - case ArrowType.left: - case ArrowType.right: - lineStartingPoint = Offset(0, originY); - lineEndingPoint = Offset(size.width, originY); - break; - } - } - canvas.drawLine(lineStartingPoint, lineEndingPoint, paint); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart deleted file mode 100644 index cc5e40cc5df..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/dimension.dart +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:flutter/material.dart'; - -import 'theme.dart'; - -/// Text widget for displaying width / height. -Widget dimensionDescription( - TextSpan description, - bool overflow, - ColorScheme colorScheme, -) { - final text = Text.rich( - description, - textAlign: TextAlign.center, - style: overflow - ? overflowingDimensionIndicatorTextStyle(colorScheme) - : dimensionIndicatorTextStyle, - overflow: TextOverflow.ellipsis, - ); - if (overflow) { - return Container( - padding: const EdgeInsets.symmetric( - vertical: minPadding, - horizontal: overflowTextHorizontalPadding, - ), - decoration: BoxDecoration( - color: colorScheme.overflowBackgroundColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: Center(child: text), - ); - } - return text; -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart deleted file mode 100644 index d515ed554fc..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/free_space.dart +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_data_models.dart'; -import 'arrow.dart'; -import 'dimension.dart'; -import 'theme.dart'; - -class FreeSpaceVisualizerWidget extends StatelessWidget { - const FreeSpaceVisualizerWidget(this.renderProperties, {super.key}); - - final RenderProperties renderProperties; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final heightDescription = - 'h=${toStringAsFixed(renderProperties.realHeight)}'; - final widthDescription = 'w=${toStringAsFixed(renderProperties.realWidth)}'; - final showWidth = - renderProperties.realWidth != renderProperties.layoutProperties.width; - final widthWidget = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: widthDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: widthIndicatorColor, - direction: Axis.horizontal, - arrowHeadSize: arrowHeadSize, - ), - ), - ], - ); - final heightWidget = SizedBox( - width: heightOnlyIndicatorSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: heightDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: heightIndicatorColor, - direction: Axis.vertical, - arrowHeadSize: arrowHeadSize, - childMarginFromArrow: 0.0, - ), - ), - ], - ), - ); - return Positioned( - top: renderProperties.offset.dy, - left: renderProperties.offset.dx, - child: SizedBox( - width: renderProperties.width, - height: renderProperties.height, - child: DevToolsTooltip( - message: '$widthDescription\n$heightDescription', - child: showWidth ? widthWidget : heightWidget, - ), - ), - ); - } -} - -class PaddingVisualizerWidget extends StatelessWidget { - const PaddingVisualizerWidget( - this.renderProperties, { - required this.horizontal, - super.key, - }); - - final RenderProperties renderProperties; - final bool horizontal; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final heightDescription = - 'h=${toStringAsFixed(renderProperties.realHeight)}'; - final widthDescription = 'w=${toStringAsFixed(renderProperties.realWidth)}'; - final widthWidget = Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: widthDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(vertical: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: widthIndicatorColor, - direction: Axis.horizontal, - arrowHeadSize: arrowHeadSize, - ), - ), - ], - ); - final heightWidget = SizedBox( - width: heightOnlyIndicatorSize, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible( - child: dimensionDescription( - TextSpan(text: heightDescription), - false, - colorScheme, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: arrowMargin), - child: const ArrowWrapper.bidirectional( - arrowColor: heightIndicatorColor, - direction: Axis.vertical, - arrowHeadSize: arrowHeadSize, - childMarginFromArrow: 0.0, - ), - ), - ], - ), - ); - return Positioned( - top: renderProperties.offset.dy, - left: renderProperties.offset.dx, - child: SizedBox( - width: safePositiveDouble(renderProperties.width), - height: safePositiveDouble(renderProperties.height), - child: horizontal ? widthWidget : heightWidget, - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart deleted file mode 100644 index ac4229c49b7..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/layout_explorer_widget.dart +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:async'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/diagnostics/inspector_service.dart'; -import '../../../../shared/globals.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_controller.dart'; -import '../../inspector_data_models.dart'; -import 'utils.dart'; - -const maxRequestsPerSecond = 3.0; - -/// Base class for layout widgets for all widget types. -abstract class LayoutExplorerWidget extends StatefulWidget { - const LayoutExplorerWidget(this.inspectorController, {super.key}); - - final InspectorController inspectorController; -} - -/// Base class for state objects for layout widgets for all widget types. -abstract class LayoutExplorerWidgetState< - W extends LayoutExplorerWidget, - L extends LayoutProperties -> - extends State - with TickerProviderStateMixin - implements InspectorServiceClient { - LayoutExplorerWidgetState() { - _onSelectionChangedCallback = onSelectionChanged; - } - - late AnimationController entranceController; - late CurvedAnimation entranceCurve; - late AnimationController changeController; - - late CurvedAnimation changeAnimation; - - L? _previousProperties; - - L? _properties; - - InspectorObjectGroupManager? objectGroupManager; - - AnimatedLayoutProperties? get animatedProperties => _animatedProperties; - AnimatedLayoutProperties? _animatedProperties; - - L? get properties => - _previousProperties ?? _animatedProperties as L? ?? _properties; - - RemoteDiagnosticsNode? get selectedNode => - inspectorController.selectedNode.value?.diagnostic; - - InspectorController get inspectorController => widget.inspectorController; - - InspectorService? get inspectorService => - serviceConnection.inspectorService as InspectorService?; - - late RateLimiter rateLimiter; - - late Future Function() _onSelectionChangedCallback; - - Future onSelectionChanged() async { - if (!mounted) return; - final selectedNodeLocal = selectedNode; - if (selectedNodeLocal == null) return; - if (!shouldDisplay(selectedNodeLocal)) return; - final prevRootId = id(_properties?.node); - final newRootId = id(getRoot(selectedNodeLocal)); - final shouldFetch = prevRootId != newRootId; - if (shouldFetch) { - _dirty = false; - final newSelection = await fetchLayoutProperties(); - _setProperties(newSelection); - } else { - updateHighlighted(_properties); - } - } - - /// Whether this layout explorer can work with this kind of node. - bool shouldDisplay(RemoteDiagnosticsNode node); - - List get children => properties!.displayChildren; - - LayoutProperties? highlighted; - - /// Returns the root widget to show. - /// - /// For cases such as Flex widgets or in the future ListView widgets we may - /// want to show the layout for all widgets under a root that is the parent - /// of the current widget. - RemoteDiagnosticsNode? getRoot(RemoteDiagnosticsNode? node); - - Future fetchLayoutProperties() async { - objectGroupManager?.cancelNext(); - final manager = objectGroupManager!; - final nextObjectGroup = manager.next; - final node = await nextObjectGroup.getLayoutExplorerNode( - getRoot(selectedNode), - ); - if (node == null || node.renderObject == null) return null; - - if (!nextObjectGroup.disposed) { - assert(manager.next == nextObjectGroup); - manager.promoteNext(); - } - return computeLayoutProperties(node); - } - - L computeLayoutProperties(RemoteDiagnosticsNode node); - - AnimatedLayoutProperties computeAnimatedProperties(L nextProperties); - - void updateHighlighted(L? newProperties); - - String? id(RemoteDiagnosticsNode? node) => node?.valueRef.id; - - void _registerInspectorControllerService() { - inspectorController.selectedNode.addListener(_onSelectionChangedCallback); - inspectorService?.addClient(this); - } - - void _unregisterInspectorControllerService() { - inspectorController.selectedNode.removeListener( - _onSelectionChangedCallback, - ); - inspectorService?.removeClient(this); - } - - @override - void initState() { - super.initState(); - rateLimiter = RateLimiter(maxRequestsPerSecond, refresh); - _registerInspectorControllerService(); - _initAnimationStates(); - _updateObjectGroupManager(); - // TODO(jacobr): put inspector controller in Controllers and - // update on didChangeDependencies. - _animateProperties(); - } - - @override - void didUpdateWidget(W oldWidget) { - super.didUpdateWidget(oldWidget); - _updateObjectGroupManager(); - _animateProperties(); - if (oldWidget.inspectorController != inspectorController) { - _unregisterInspectorControllerService(); - _registerInspectorControllerService(); - } - } - - @override - void dispose() { - entranceController.dispose(); - changeController.dispose(); - _unregisterInspectorControllerService(); - super.dispose(); - } - - void _animateProperties() { - if (_animatedProperties != null) { - unawaited(changeController.forward()); - } - if (_previousProperties != null) { - unawaited(entranceController.reverse()); - } else { - unawaited(entranceController.forward()); - } - } - - // update selected widget in the device without triggering selection listener event. - // this is required so that we don't change focus - // when tapping on a child is also Flex-based widget. - Future setSelectionInspector(RemoteDiagnosticsNode node) async { - final service = node.objectGroupApi; - if (service != null && service is ObjectGroup) { - await service.setSelectionInspector(node.valueRef, false); - } - } - - // update selected widget and trigger selection listener event to change focus. - void refreshSelection(RemoteDiagnosticsNode node) { - inspectorController.refreshSelection(node, node, true); - } - - Future onTap(LayoutProperties properties) async { - setState(() => highlighted = properties); - await setSelectionInspector(properties.node); - } - - void onDoubleTap(LayoutProperties properties) { - refreshSelection(properties.node); - } - - Future refresh() async { - if (!_dirty) return; - _dirty = false; - final updatedProperties = await fetchLayoutProperties(); - if (updatedProperties != null) { - _changeProperties(updatedProperties); - } - } - - void _changeProperties(L nextProperties) { - if (!mounted) return; - updateHighlighted(nextProperties); - setState(() { - _animatedProperties = computeAnimatedProperties(nextProperties); - unawaited(changeController.forward(from: 0.0)); - }); - } - - void _setProperties(L? newProperties) { - if (!mounted) return; - updateHighlighted(newProperties); - if (_properties == newProperties) { - return; - } - setState(() { - _previousProperties ??= _properties; - _properties = newProperties; - }); - _animateProperties(); - } - - void _initAnimationStates() { - entranceController = longAnimationController(this) - ..addStatusListener((status) { - if (status == AnimationStatus.dismissed) { - setState(() { - _previousProperties = null; - unawaited(entranceController.forward()); - }); - } - }); - entranceCurve = defaultCurvedAnimation(entranceController); - changeController = longAnimationController(this) - ..addStatusListener((status) { - if (status == AnimationStatus.completed) { - setState(() { - _properties = _animatedProperties!.end; - _animatedProperties = null; - changeController.value = 0.0; - }); - } - }); - changeAnimation = defaultCurvedAnimation(changeController); - } - - void _updateObjectGroupManager() { - final service = serviceConnection.inspectorService; - if (service != objectGroupManager?.inspectorService) { - objectGroupManager = InspectorObjectGroupManager( - service as InspectorService, - 'flex-layout', - ); - } - unawaited(onSelectionChanged()); - } - - bool _dirty = false; - - @override - void onFlutterFrame() { - if (!mounted) return; - if (_dirty) { - rateLimiter.scheduleRequest(); - } - } - - // TODO(albertusangga): Investigate why onForceRefresh is not getting called. - @override - Future onForceRefresh() async { - final properties = await fetchLayoutProperties(); - if (properties != null) { - _setProperties(properties); - } - } - - /// Currently this is not working so we should listen to controller selection event instead. - @override - Future onInspectorSelectionChanged() async {} - - /// Register callback to be executed once Flutter frame is ready. - void markAsDirty() { - _dirty = true; - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart deleted file mode 100644 index 298b7782ef8..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/overflow_indicator_painter.dart +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:ui' as ui; - -import 'package:flutter/rendering.dart'; - -import '../../inspector_data_models.dart'; - -/// CustomPainter for drawing [DebugOverflowIndicatorMixin]'s patterned background. -/// Draws overflow pattern on the [OverflowSide] of the widget. -/// -/// [DebugOverflowIndicatorMixin] can not be reused here -/// because it is a mixin on RenderObject and requires real overflows on the widget. -/// -/// If [side] is set to [OverflowSide.right], -/// the pattern will occupy the whole height -/// and the width will be the given [size]. -/// -/// If [side] is set to [OverflowSide.bottom], -/// the pattern will occupy the whole width -/// and the height will be the given [size]. -/// -/// See also: -/// * [DebugOverflowIndicatorMixin] -class OverflowIndicatorPainter extends CustomPainter { - const OverflowIndicatorPainter(this.side, this.size); - - final OverflowSide side; - final double size; - - /// These static variables are taken from [DebugOverflowIndicatorMixin] - /// since all of them are private. - static const black = Color(0xBF000000); - static const yellow = Color(0xBFFFFF00); - static final indicatorPaint = Paint() - ..shader = ui.Gradient.linear( - const Offset(0.0, 0.0), - const Offset(10.0, 10.0), - [black, yellow, yellow, black], - [0.25, 0.25, 0.75, 0.75], - TileMode.repeated, - ); - - @override - void paint(Canvas canvas, Size size) { - final bottomOverflow = OverflowSide.bottom == side; - final width = bottomOverflow ? size.width : this.size; - final height = !bottomOverflow ? size.height : this.size; - - final left = bottomOverflow ? 0.0 : size.width - width; - final top = side == OverflowSide.right ? 0.0 : size.height - height; - final rect = Rect.fromLTWH(left, top, width, height); - canvas.drawRect(rect, indicatorPaint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate is OverflowIndicatorPainter && - (side != oldDelegate.side || size != oldDelegate.size); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart deleted file mode 100644 index 974a0a5be75..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/theme.dart +++ /dev/null @@ -1,139 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -const margin = 6.0; - -const arrowHeadSize = 8.0; -const arrowMargin = 4.0; -const arrowStrokeWidth = 1.5; - -/// Hardcoded sizes for scaling the flex children widget properly. -const minRenderWidth = 250.0; -const minRenderHeight = 250.0; - -const minPadding = 2.0; -const overflowTextHorizontalPadding = 8.0; - -/// The size to shrink a widget by when animating it in. -const entranceMargin = 50.0; - -const defaultMaxRenderWidth = 400.0; -const defaultMaxRenderHeight = 400.0; - -const widgetTitleMaxWidthPercentage = 0.75; - -/// Hardcoded arrow size respective to its cross axis (because it's unconstrained). -const heightAndConstraintIndicatorSize = 48.0; -const widthAndConstraintIndicatorSize = 56.0; -const mainAxisArrowIndicatorSize = 48.0; -const crossAxisArrowIndicatorSize = 48.0; - -const heightOnlyIndicatorSize = 72.0; - -/// Minimum size to display width/height inside the arrow -const minWidthToDisplayWidthInsideArrow = 200.0; -const minHeightToDisplayHeightInsideArrow = 200.0; - -const smallTextScaleFactor = 0.8; - -/// Height for limiting asset image (selected one in the drop down). -const axisAlignmentAssetImageHeight = 24.0; - -const minHeightToAllowTruncating = 375.0; -const minWidthToAllowTruncating = 375.0; - -// Story of Layout colors -const mainAxisLightColor = Color(0xff2c5daa); -const mainAxisDarkColor = Color(0xff2c5daa); - -const textColor = Color(0xff55767f); -const emphasizedTextColor = Color(0xff009aca); - -const crossAxisLightColor = Color(0xff8ac652); -const crossAxisDarkColor = Color(0xff8ac652); - -const mainAxisTextColorLight = Color(0xFF1375bc); -const mainAxisTextColorDark = Color(0xFF1375bc); - -const crossAxisTextColorLight = Color(0xFF66672C); -const crossAxisTextColorsDark = Color(0xFFB3D25A); - -const overflowBackgroundColorDark = Color(0xFFB00020); -const overflowBackgroundColorLight = Color(0xFFB00020); - -const overflowTextColorDark = Color(0xfff5846b); -const overflowTextColorLight = Color(0xffdea089); - -const backgroundColorSelectedDark = Color( - 0x4d474747, -); // TODO(jacobr): we would like Color(0x4dedeeef) but that makes the background show through. -const backgroundColorSelectedLight = Color(0x4dedeeef); - -extension LayoutExplorerColorScheme on ColorScheme { - Color get mainAxisColor => isLight ? mainAxisLightColor : mainAxisDarkColor; - - Color get widgetNameColor => isLight ? Colors.white : Colors.black; - - Color get crossAxisColor => - isLight ? crossAxisLightColor : crossAxisDarkColor; - - Color get mainAxisTextColor => - isLight ? mainAxisTextColorLight : mainAxisTextColorDark; - - Color get crossAxisTextColor => - isLight ? crossAxisTextColorLight : crossAxisTextColorsDark; - - Color get overflowBackgroundColor => - isLight ? overflowBackgroundColorLight : overflowBackgroundColorDark; - - Color get overflowTextColor => - isLight ? overflowTextColorLight : overflowTextColorDark; - - Color get backgroundColorSelected => - isLight ? backgroundColorSelectedLight : backgroundColorSelectedDark; - - Color get unconstrainedColor => - isLight ? unconstrainedLightColor : unconstrainedDarkColor; -} - -const backgroundColorDark = Color(0xff30302f); -const backgroundColorLight = Color(0xffffffff); - -const unconstrainedDarkColor = Color(0xffdea089); -const unconstrainedLightColor = Color(0xfff5846b); - -const widthIndicatorColor = textColor; -const heightIndicatorColor = textColor; - -const negativeSpaceDarkAssetName = - 'assets/img/layout_explorer/negative_space_dark.png'; -const negativeSpaceLightAssetName = - 'assets/img/layout_explorer/negative_space_light.png'; - -const dimensionIndicatorTextStyle = TextStyle( - height: 1.0, - letterSpacing: 1.1, - color: emphasizedTextColor, - fontSize: defaultFontSize, -); - -TextStyle overflowingDimensionIndicatorTextStyle(ColorScheme colorScheme) => - dimensionIndicatorTextStyle.merge( - TextStyle( - fontWeight: FontWeight.bold, - color: colorScheme.overflowTextColor, - ), - ); - -Widget buildUnderline() { - return Container( - height: 1.0, - decoration: const BoxDecoration( - border: Border(bottom: BorderSide(color: textColor, width: 0.0)), - ), - ); -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart deleted file mode 100644 index 798ea7a45f7..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/utils.dart +++ /dev/null @@ -1,439 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:ui'; - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/diagnostics/diagnostics_node.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_data_models.dart'; -import 'overflow_indicator_painter.dart'; -import 'theme.dart'; -import 'widgets_theme.dart'; - -/// A widget for positioning sized widgets that follows layout as follows: -/// | top | -/// left | center | right -/// | bottom | -@immutable -class BorderLayout extends StatelessWidget { - const BorderLayout({ - super.key, - this.left, - this.leftWidth, - this.top, - this.topHeight, - this.right, - this.rightWidth, - this.bottom, - this.bottomHeight, - this.center, - }) : assert( - left != null || - top != null || - right != null || - bottom != null || - center != null, - ); - - final Widget? center; - final Widget? top; - final Widget? left; - final Widget? right; - final Widget? bottom; - - final double? leftWidth; - final double? rightWidth; - final double? topHeight; - final double? bottomHeight; - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - Center( - child: Container( - margin: EdgeInsets.only( - left: leftWidth ?? 0, - right: rightWidth ?? 0, - top: topHeight ?? 0, - bottom: bottomHeight ?? 0, - ), - child: center, - ), - ), - if (top != null) - Align( - alignment: Alignment.topCenter, - child: SizedBox(height: topHeight, child: top), - ), - if (left != null) - Align( - alignment: Alignment.centerLeft, - child: SizedBox(width: leftWidth, child: left), - ), - if (right != null) - Align( - alignment: Alignment.centerRight, - child: SizedBox(width: rightWidth, child: right), - ), - if (bottom != null) - Align( - alignment: Alignment.bottomCenter, - child: SizedBox(height: bottomHeight, child: bottom), - ), - ], - ); - } -} - -@immutable -class Truncateable extends StatelessWidget { - const Truncateable({super.key, this.truncate = false, required this.child}); - - final Widget child; - final bool truncate; - - @override - Widget build(BuildContext context) { - return Flexible(flex: truncate ? 1 : 0, child: child); - } -} - -/// Widget that draws bounding box with the title (usually widget name) in its -/// top left. -/// -/// * [hint] is an optional widget to be placed in the top right of the box. -/// * [child] is an optional widget to be placed in the center of the box. -class WidgetVisualizer extends StatelessWidget { - const WidgetVisualizer({ - super.key, - required this.title, - this.hint, - required this.isSelected, - required this.layoutProperties, - required this.child, - this.overflowSide, - this.largeTitle = false, - }); - - final LayoutProperties layoutProperties; - final String title; - final Widget child; - final Widget? hint; - final bool isSelected; - final bool largeTitle; - - final OverflowSide? overflowSide; - - static const _overflowIndicatorSize = 20.0; - static const _borderUnselectedWidth = 1.0; - static const _borderSelectedWidth = 3.0; - static const _selectedPadding = 4.0; - - bool get drawOverflow => overflowSide != null; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final colorScheme = theme.colorScheme; - final properties = layoutProperties; - final borderColor = WidgetTheme.fromName(properties.node.description).color; - final boxAdjust = isSelected ? _selectedPadding : 0.0; - - return LayoutBuilder( - builder: (context, constraints) { - final hintLocal = hint; - return OverflowBox( - minWidth: constraints.minWidth + boxAdjust, - maxWidth: constraints.maxWidth + boxAdjust, - maxHeight: constraints.maxHeight + boxAdjust, - minHeight: constraints.minHeight + boxAdjust, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: borderColor, - width: isSelected - ? _borderSelectedWidth - : _borderUnselectedWidth, - ), - color: isSelected - ? theme.canvasColor.brighten() - : theme.canvasColor.darken(), - boxShadow: isSelected - ? [ - BoxShadow( - color: Colors.black.withAlpha(255 ~/ 2), - blurRadius: 20, - ), - ] - : null, - ), - child: Stack( - children: [ - if (drawOverflow) - Positioned.fill( - child: CustomPaint( - painter: OverflowIndicatorPainter( - overflowSide!, - _overflowIndicatorSize, - ), - ), - ), - Container( - margin: EdgeInsets.only( - right: overflowSide == OverflowSide.right - ? _overflowIndicatorSize - : 0.0, - bottom: overflowSide == OverflowSide.bottom - ? _overflowIndicatorSize - : 0.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Container( - constraints: BoxConstraints( - maxWidth: largeTitle - ? defaultMaxRenderWidth - : minRenderWidth * - widgetTitleMaxWidthPercentage, - ), - decoration: BoxDecoration(color: borderColor), - padding: const EdgeInsets.all(4.0), - child: Center( - child: Text( - title, - style: theme.regularTextStyleWithColor( - colorScheme.widgetNameColor, - ), - overflow: TextOverflow.ellipsis, - ), - ), - ), - ), - if (hintLocal != null) Flexible(child: hintLocal), - ], - ), - ), - Expanded(child: child), - ], - ), - ), - ], - ), - ), - ); - }, - ); - } -} - -class AnimatedLayoutProperties - implements LayoutProperties { - AnimatedLayoutProperties(this.begin, this.end, this.animation) - : assert(begin.children.length == end.children.length), - _children = [ - for (var i = 0; i < begin.children.length; i++) - AnimatedLayoutProperties( - begin.children[i], - end.children[i], - animation, - ), - ]; - - final T begin; - final T end; - final Animation animation; - final List _children; - - @override - LayoutProperties? get parent => end.parent; - - @override - set parent(LayoutProperties? parent) { - end.parent = parent; - } - - @override - List get children { - return _children; - } - - List _lerpList(List l1, List l2) { - assert(l1.length == l2.length); - if (l1.isEmpty) return []; - final animationLocal = animation; - return [ - for (var i = 0; i < children.length; i++) - lerpDouble(l1[i], l2[i], animationLocal.value)!, - ]; - } - - @override - List childrenDimensions(Axis axis) { - final beginDimensions = begin.childrenDimensions(axis); - final endDimensions = end.childrenDimensions(axis); - return _lerpList(beginDimensions, endDimensions); - } - - @override - List get childrenHeights => - _lerpList(begin.childrenHeights, end.childrenHeights); - - @override - List get childrenWidths => - _lerpList(begin.childrenWidths, end.childrenWidths); - - @override - BoxConstraints? get constraints { - try { - return BoxConstraints.lerp( - begin.constraints, - end.constraints, - animation.value, - ); - } catch (e) { - return end.constraints; - } - } - - @override - String describeWidthConstraints() { - final constraintsLocal = constraints!; - return constraintsLocal.hasBoundedWidth - ? LayoutProperties.describeAxis( - constraintsLocal.minWidth, - constraintsLocal.maxWidth, - 'w', - ) - : 'w=unconstrained'; - } - - @override - String describeHeightConstraints() { - final constraintsLocal = constraints!; - return constraintsLocal.hasBoundedHeight - ? LayoutProperties.describeAxis( - constraintsLocal.minHeight, - constraintsLocal.maxHeight, - 'h', - ) - : 'h=unconstrained'; - } - - @override - String describeWidth() => 'w=${toStringAsFixed(size.width)}'; - - @override - String describeHeight() => 'h=${toStringAsFixed(size.height)}'; - - @override - String? get description => end.description; - - @override - double dimension(Axis axis) { - return lerpDouble( - begin.dimension(axis), - end.dimension(axis), - animation.value, - )!; - } - - @override - num? get flexFactor => - lerpDouble(begin.flexFactor, end.flexFactor, animation.value); - - @override - bool get hasChildren => children.isNotEmpty; - - @override - double get height => size.height; - - @override - bool get isFlex => begin.isFlex && end.isFlex; - - @override - RemoteDiagnosticsNode get node => end.node; - - @override - Size get size { - return Size.lerp(begin.size, end.size, animation.value)!; - } - - @override - int get totalChildren => end.totalChildren; - - @override - double get width => size.width; - - @override - bool get hasFlexFactor => begin.hasFlexFactor && end.hasFlexFactor; - - @override - LayoutProperties copyWith({ - List? children, - BoxConstraints? constraints, - String? description, - int? flexFactor, - FlexFit? flexFit, - bool? isFlex, - Size? size, - }) { - return LayoutProperties.values( - node: node, - children: children ?? this.children, - constraints: constraints ?? this.constraints, - description: description ?? this.description, - flexFactor: flexFactor ?? this.flexFactor, - flexFit: flexFit ?? this.flexFit, - isFlex: isFlex ?? this.isFlex, - size: size ?? this.size, - ); - } - - @override - bool get isOverflowWidth => end.isOverflowWidth; - - @override - bool get isOverflowHeight => end.isOverflowHeight; - - @override - FlexFit? get flexFit => end.flexFit; - - @override - List get displayChildren => end.displayChildren; -} - -class LayoutExplorerBackground extends StatelessWidget { - const LayoutExplorerBackground({super.key, required this.colorScheme}); - - final ColorScheme colorScheme; - - @override - Widget build(BuildContext context) { - return Positioned.fill( - child: Opacity( - opacity: colorScheme.isLight ? 0.3 : 0.2, - child: Image.asset( - colorScheme.isLight - ? negativeSpaceLightAssetName - : negativeSpaceDarkAssetName, - fit: BoxFit.none, - repeat: ImageRepeat.repeat, - alignment: Alignment.topLeft, - ), - ), - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart deleted file mode 100644 index 45de4048974..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widget_constraints.dart +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app_shared/ui.dart'; -import 'package:flutter/material.dart'; - -import '../../../../shared/primitives/math_utils.dart'; -import '../../../../shared/primitives/utils.dart'; -import '../../inspector_data_models.dart'; -import 'arrow.dart'; -import 'dimension.dart'; -import 'theme.dart'; -import 'utils.dart'; - -class VisualizeWidthAndHeightWithConstraints extends StatelessWidget { - const VisualizeWidthAndHeightWithConstraints({ - super.key, - required this.properties, - double? arrowHeadSize, - required this.child, - this.warnIfUnconstrained = true, - }) : arrowHeadSize = arrowHeadSize ?? defaultIconSize; - - final Widget child; - final LayoutProperties properties; - final double arrowHeadSize; - final bool warnIfUnconstrained; - - @override - Widget build(BuildContext context) { - final propertiesLocal = properties; - final showChildrenWidthsSum = - propertiesLocal is FlexLayoutProperties && - propertiesLocal.isOverflowWidth; - const bottomHeight = widthAndConstraintIndicatorSize; - const rightWidth = heightAndConstraintIndicatorSize; - final colorScheme = Theme.of(context).colorScheme; - - final showOverflowHeight = - properties is FlexLayoutProperties && propertiesLocal.isOverflowHeight; - final heightDescription = RotatedBox( - quarterTurns: 1, - child: dimensionDescription( - TextSpan( - children: [ - TextSpan(text: propertiesLocal.describeHeight()), - if (propertiesLocal.constraints != null) ...[ - if (!showOverflowHeight) const TextSpan(text: '\n'), - TextSpan( - text: ' (${propertiesLocal.describeHeightConstraints()})', - style: - propertiesLocal.constraints!.hasBoundedHeight || - !warnIfUnconstrained - ? null - : TextStyle(color: colorScheme.unconstrainedColor), - ), - ], - if (showOverflowHeight) - TextSpan( - text: - '\nchildren take: ' - '${toStringAsFixed(sum(propertiesLocal.childrenHeights))}', - ), - ], - ), - propertiesLocal.isOverflowHeight, - colorScheme, - ), - ); - final right = Container( - margin: const EdgeInsets.only( - top: margin, - left: margin, - bottom: bottomHeight, - right: minPadding, // custom margin for not sticking to the corner - ), - child: LayoutBuilder( - builder: (context, constraints) { - final displayHeightOutsideArrow = - constraints.maxHeight < minHeightToDisplayHeightInsideArrow; - return Row( - children: [ - Truncateable( - truncate: !displayHeightOutsideArrow, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: arrowMargin), - child: ArrowWrapper.bidirectional( - arrowColor: heightIndicatorColor, - arrowStrokeWidth: arrowStrokeWidth, - arrowHeadSize: arrowHeadSize, - direction: Axis.vertical, - child: displayHeightOutsideArrow ? null : heightDescription, - ), - ), - ), - if (displayHeightOutsideArrow) Flexible(child: heightDescription), - ], - ); - }, - ), - ); - - final widthDescription = dimensionDescription( - TextSpan( - children: [ - TextSpan(text: '${propertiesLocal.describeWidth()}; '), - if (propertiesLocal.constraints != null) ...[ - if (!showChildrenWidthsSum) const TextSpan(text: '\n'), - TextSpan( - text: '(${propertiesLocal.describeWidthConstraints()})', - style: - propertiesLocal.constraints!.hasBoundedWidth || - !warnIfUnconstrained - ? null - : TextStyle(color: colorScheme.unconstrainedColor), - ), - ], - if (showChildrenWidthsSum) - TextSpan( - text: - '\nchildren take ' - '${toStringAsFixed(sum(propertiesLocal.childrenWidths))}', - ), - ], - ), - propertiesLocal.isOverflowWidth, - colorScheme, - ); - final bottom = Container( - margin: const EdgeInsets.only( - top: margin, - left: margin, - right: rightWidth, - bottom: minPadding, - ), - child: LayoutBuilder( - builder: (context, constraints) { - final maxWidth = constraints.maxWidth; - final displayWidthOutsideArrow = - maxWidth < minWidthToDisplayWidthInsideArrow; - return Column( - children: [ - Truncateable( - truncate: !displayWidthOutsideArrow, - child: Container( - margin: const EdgeInsets.symmetric(vertical: arrowMargin), - child: ArrowWrapper.bidirectional( - arrowColor: widthIndicatorColor, - arrowHeadSize: arrowHeadSize, - arrowStrokeWidth: arrowStrokeWidth, - direction: Axis.horizontal, - child: displayWidthOutsideArrow ? null : widthDescription, - ), - ), - ), - if (displayWidthOutsideArrow) - Flexible( - child: Container( - padding: const EdgeInsets.only(top: minPadding), - child: widthDescription, - ), - ), - ], - ); - }, - ), - ); - return BorderLayout( - center: child, - right: right, - rightWidth: rightWidth, - bottom: bottom, - bottomHeight: bottomHeight, - ); - } -} diff --git a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart b/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart deleted file mode 100644 index 4abdb55e500..00000000000 --- a/packages/devtools_app/lib/src/screens/inspector/layout_explorer/ui/widgets_theme.dart +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:flutter/material.dart'; - -class WidgetTheme { - const WidgetTheme({this.iconAsset, this.color = otherWidgetColor}); - - final String? iconAsset; - final Color color; - - static WidgetTheme fromName(String? widgetType) { - if (widgetType == null) { - return const WidgetTheme(); - } - - return themeMap[_stripBrackets(widgetType)] ?? const WidgetTheme(); - } - - /// Strips the brackets off the widget name. - /// - /// For example: `AnimatedBuilder` -> `AnimatedBuilder`. - static String _stripBrackets(String widgetType) { - final bracketIndex = widgetType.indexOf('<'); - if (bracketIndex == -1) { - return widgetType; - } - - return widgetType.substring(0, bracketIndex); - } - - static const contentWidgetColor = Color(0xff06AC3B); - static const highLevelWidgetColor = Color(0xffAEAEB1); - static const animationWidgetColor = Color(0xffE09D0E); - static const otherWidgetColor = Color(0xff0EA7E0); - - static const animatedTheme = WidgetTheme( - iconAsset: WidgetIcons.animated, - color: animationWidgetColor, - ); - - static const transitionTheme = WidgetTheme( - iconAsset: WidgetIcons.transition, - color: animationWidgetColor, - ); - - static const textTheme = WidgetTheme( - iconAsset: WidgetIcons.text, - color: contentWidgetColor, - ); - - static const imageTheme = WidgetTheme( - iconAsset: WidgetIcons.image, - color: contentWidgetColor, - ); - - static const tabTheme = WidgetTheme(iconAsset: WidgetIcons.tab); - static const scrollTheme = WidgetTheme(iconAsset: WidgetIcons.scroll); - static const highLevelTheme = WidgetTheme(color: highLevelWidgetColor); - static const listTheme = WidgetTheme(iconAsset: WidgetIcons.listView); - static const expandTheme = WidgetTheme(iconAsset: WidgetIcons.expand); - static const alignTheme = WidgetTheme(iconAsset: WidgetIcons.align); - static const gestureTheme = WidgetTheme(iconAsset: WidgetIcons.gesture); - static const textButtonTheme = WidgetTheme(iconAsset: WidgetIcons.textButton); - static const toggleTheme = WidgetTheme( - iconAsset: WidgetIcons.toggle, - color: contentWidgetColor, - ); - - static const themeMap = { - // High-level - 'RenderObjectToWidgetAdapter': WidgetTheme( - iconAsset: WidgetIcons.root, - color: highLevelWidgetColor, - ), - 'CupertinoApp': highLevelTheme, - 'MaterialApp': WidgetTheme(iconAsset: WidgetIcons.materialApp), - 'WidgetsApp': highLevelTheme, - - // Text - 'DefaultTextStyle': textTheme, - 'RichText': textTheme, - 'SelectableText': textTheme, - 'Text': textTheme, - - // Images - 'Icon': imageTheme, - 'Image': imageTheme, - 'RawImage': imageTheme, - - // Animations - 'AnimatedAlign': animatedTheme, - 'AnimatedBuilder': animatedTheme, - 'AnimatedContainer': animatedTheme, - 'AnimatedCrossFade': animatedTheme, - 'AnimatedDefaultTextStyle': animatedTheme, - 'AnimatedListState': animatedTheme, - 'AnimatedModalBarrier': animatedTheme, - 'AnimatedOpacity': animatedTheme, - 'AnimatedPhysicalModel': animatedTheme, - 'AnimatedPositioned': animatedTheme, - 'AnimatedSize': animatedTheme, - 'AnimatedWidget': animatedTheme, - - // Transitions - 'DecoratedBoxTransition': transitionTheme, - 'FadeTransition': transitionTheme, - 'PositionedTransition': transitionTheme, - 'RotationTransition': transitionTheme, - 'ScaleTransition': transitionTheme, - 'SizeTransition': transitionTheme, - 'SlideTransition': transitionTheme, - 'Hero': WidgetTheme( - iconAsset: WidgetIcons.hero, - color: animationWidgetColor, - ), - - // Scroll - 'CustomScrollView': scrollTheme, - 'DraggableScrollableSheet': scrollTheme, - 'SingleChildScrollView': scrollTheme, - 'Scrollable': scrollTheme, - 'Scrollbar': scrollTheme, - 'ScrollConfiguration': scrollTheme, - 'GridView': WidgetTheme(iconAsset: WidgetIcons.gridView), - 'ListView': listTheme, - 'ReorderableListView': listTheme, - 'NestedScrollView': listTheme, - - // Input - 'Checkbox': WidgetTheme( - iconAsset: WidgetIcons.checkbox, - color: contentWidgetColor, - ), - 'Radio': WidgetTheme( - iconAsset: WidgetIcons.radio, - color: contentWidgetColor, - ), - 'Switch': toggleTheme, - 'CupertinoSwitch': toggleTheme, - - // Layout - 'Container': WidgetTheme(iconAsset: WidgetIcons.container), - 'Center': WidgetTheme(iconAsset: WidgetIcons.center), - 'Row': WidgetTheme(iconAsset: WidgetIcons.row), - 'Column': WidgetTheme(iconAsset: WidgetIcons.column), - 'Padding': WidgetTheme(iconAsset: WidgetIcons.padding), - 'SizedBox': WidgetTheme(iconAsset: WidgetIcons.sizedBox), - 'ConstrainedBox': WidgetTheme(iconAsset: WidgetIcons.constrainedBox), - 'Align': alignTheme, - 'Positioned': alignTheme, - 'Expanded': expandTheme, - 'Flexible': expandTheme, - 'Stack': WidgetTheme(iconAsset: WidgetIcons.stack), - 'Wrap': WidgetTheme(iconAsset: WidgetIcons.wrap), - - // Buttons - 'FloatingActionButton': WidgetTheme( - iconAsset: WidgetIcons.floatingActionButton, - color: contentWidgetColor, - ), - 'InkWell': WidgetTheme(iconAsset: WidgetIcons.inkWell), - 'GestureDetector': gestureTheme, - 'RawGestureDetector': gestureTheme, - 'TextButton': textButtonTheme, - 'CupertinoButton': textButtonTheme, - 'ElevatedButton': textButtonTheme, - 'OutlinedButton': WidgetTheme(iconAsset: WidgetIcons.outlinedButton), - - // Tabs - 'Tab': tabTheme, - 'TabBar': tabTheme, - 'TabBarView': tabTheme, - 'BottomNavigationBar': WidgetTheme( - iconAsset: WidgetIcons.bottomNavigationBar, - ), - 'CupertinoTabScaffold': tabTheme, - 'CupertinoTabView': tabTheme, - - // Other - 'Scaffold': WidgetTheme(iconAsset: WidgetIcons.scaffold), - 'CircularProgressIndicator': WidgetTheme( - iconAsset: WidgetIcons.circularProgress, - ), - 'Card': WidgetTheme(iconAsset: WidgetIcons.card), - 'Divider': WidgetTheme(iconAsset: WidgetIcons.divider), - 'AlertDialog': WidgetTheme(iconAsset: WidgetIcons.alertDialog), - 'CircleAvatar': WidgetTheme(iconAsset: WidgetIcons.circleAvatar), - 'Opacity': WidgetTheme(iconAsset: WidgetIcons.opacity), - 'Drawer': WidgetTheme(iconAsset: WidgetIcons.drawer), - 'PageView': WidgetTheme(iconAsset: WidgetIcons.pageView), - 'Material': WidgetTheme(iconAsset: WidgetIcons.material), - 'AppBar': WidgetTheme(iconAsset: WidgetIcons.appBar), - 'HiddenGroup': WidgetTheme(iconAsset: WidgetIcons.hidden), - }; -} - -class WidgetIcons { - static const root = 'icons/inspector/widget_icons/root.png'; - static const text = 'icons/inspector/widget_icons/text.png'; - static const icon = 'icons/inspector/widget_icons/icon.png'; - static const image = 'icons/inspector/widget_icons/image.png'; - static const floatingActionButton = - 'icons/inspector/widget_icons/floatingab.png'; - static const checkbox = 'icons/inspector/widget_icons/checkbox.png'; - static const radio = 'icons/inspector/widget_icons/radio.png'; - static const toggle = 'icons/inspector/widget_icons/toggle.png'; - static const animated = 'icons/inspector/widget_icons/animated.png'; - static const transition = 'icons/inspector/widget_icons/transition.png'; - static const hero = 'icons/inspector/widget_icons/hero.png'; - static const container = 'icons/inspector/widget_icons/container.png'; - static const center = 'icons/inspector/widget_icons/center.png'; - static const row = 'icons/inspector/widget_icons/row.png'; - static const column = 'icons/inspector/widget_icons/column.png'; - static const padding = 'icons/inspector/widget_icons/padding.png'; - static const scaffold = 'icons/inspector/widget_icons/scaffold.png'; - static const sizedBox = 'icons/inspector/widget_icons/sizedbox.png'; - static const align = 'icons/inspector/widget_icons/align.png'; - static const scroll = 'icons/inspector/widget_icons/scroll.png'; - static const stack = 'icons/inspector/widget_icons/stack.png'; - static const inkWell = 'icons/inspector/widget_icons/inkwell.png'; - static const gesture = 'icons/inspector/widget_icons/gesture.png'; - static const textButton = 'icons/inspector/widget_icons/textbutton.png'; - static const outlinedButton = - 'icons/inspector/widget_icons/outlinedbutton.png'; - static const gridView = 'icons/inspector/widget_icons/gridview.png'; - static const listView = 'icons/inspector/widget_icons/listView.png'; - - static const alertDialog = 'icons/inspector/widget_icons/alertdialog.png'; - static const card = 'icons/inspector/widget_icons/card.png'; - static const circleAvatar = 'icons/inspector/widget_icons/circleavatar.png'; - static const circularProgress = - 'icons/inspector/widget_icons/circularprogress.png'; - static const constrainedBox = - 'icons/inspector/widget_icons/constrainedbox.png'; - static const divider = 'icons/inspector/widget_icons/divider.png'; - static const drawer = 'icons/inspector/widget_icons/drawer.png'; - static const expand = 'icons/inspector/widget_icons/expand.png'; - static const material = 'icons/inspector/widget_icons/material.png'; - static const opacity = 'icons/inspector/widget_icons/opacity.png'; - static const tab = 'icons/inspector/widget_icons/tab.png'; - static const wrap = 'icons/inspector/widget_icons/wrap.png'; - static const pageView = 'icons/inspector/widget_icons/pageView.png'; - static const appBar = 'icons/inspector/widget_icons/appbar.png'; - static const materialApp = 'icons/inspector/widget_icons/materialapp.png'; - static const bottomNavigationBar = - 'icons/inspector/widget_icons/bottomnavigationbar.png'; - static const hidden = 'icons/inspector/widget_icons/onedot.png'; -} diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart index 710743dee72..42d5bb7f0ed 100644 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart +++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart @@ -3,14 +3,11 @@ // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. import 'package:devtools_app_shared/shared.dart'; -import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; -import '../../shared/feature_flags.dart'; import '../../shared/framework/screen.dart'; import '../../shared/globals.dart'; -import '../inspector/inspector_screen_body.dart' as legacy; -import '../inspector_v2/inspector_screen_body.dart' as v2; +import '../inspector_v2/inspector_screen_body.dart'; import 'inspector_screen_controller.dart'; class InspectorScreen extends Screen { @@ -32,58 +29,8 @@ class InspectorScreen extends Screen { String get docPageId => screenId; @override - Widget buildScreenBody(BuildContext context) => - const InspectorScreenSwitcher(); -} - -class InspectorScreenSwitcher extends StatefulWidget { - const InspectorScreenSwitcher({super.key}); - - @override - State createState() => - _InspectorScreenSwitcherState(); -} - -class _InspectorScreenSwitcherState extends State - with AutoDisposeMixin { - late InspectorScreenController controller; - - bool get shouldShowInspectorV2 => - FeatureFlags.inspectorV2.isEnabled && - !preferences.inspector.legacyInspectorEnabled.value; - - @override - void initState() { - super.initState(); - controller = screenControllers.lookup(); - addAutoDisposeListener( - preferences.inspector.legacyInspectorEnabled, - () async { - controller.legacyInspectorController.setVisibleToUser( - !shouldShowInspectorV2, - ); - await controller.v2InspectorController.setVisibleToUser( - shouldShowInspectorV2, - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: preferences.inspector.legacyInspectorEnabled, - builder: (context, _, _) { - if (shouldShowInspectorV2) { - return v2.InspectorScreenBody( - controller: controller.v2InspectorController, - ); - } - - return legacy.InspectorScreenBody( - controller: controller.legacyInspectorController, - ); - }, - ); + Widget buildScreenBody(BuildContext context) { + final controller = screenControllers.lookup(); + return InspectorScreenBody(controller: controller.inspectorController); } } diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart index 5ca8073fa6f..7656c99ae04 100644 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart @@ -6,10 +6,8 @@ import '../../shared/analytics/metrics.dart'; import '../../shared/console/primitives/simple_items.dart'; import '../../shared/framework/screen.dart'; import '../../shared/framework/screen_controllers.dart'; -import '../inspector/inspector_controller.dart' as legacy; -import '../inspector/inspector_tree_controller.dart' as legacy; -import '../inspector_v2/inspector_controller.dart' as v2; -import '../inspector_v2/inspector_tree_controller.dart' as v2; +import '../inspector_v2/inspector_controller.dart'; +import '../inspector_v2/inspector_tree_controller.dart'; /// Screen controller for the Inspector screen. /// @@ -25,46 +23,25 @@ class InspectorScreenController extends DevToolsScreenController { @override final screenId = ScreenMetaData.inspector.id; - late v2.InspectorController v2InspectorController; - late v2.InspectorTreeController v2InspectorTreeController; - - late legacy.InspectorController legacyInspectorController; - late legacy.InspectorTreeController legacyInspectorTreeController; - late legacy.InspectorTreeController legacyDetailsTreeController; + late InspectorController inspectorController; + late InspectorTreeController inspectorTreeController; @override void init() { super.init(); - v2InspectorTreeController = v2.InspectorTreeController( - gaId: InspectorScreenMetrics.summaryTreeGaId, - ); - v2InspectorController = v2.InspectorController( - inspectorTree: v2InspectorTreeController, - treeType: FlutterTreeType.widget, - ); - - // TODO(elliette): Remove legacy inspector. - legacyInspectorTreeController = legacy.InspectorTreeController( + inspectorTreeController = InspectorTreeController( gaId: InspectorScreenMetrics.summaryTreeGaId, ); - legacyDetailsTreeController = legacy.InspectorTreeController( - gaId: InspectorScreenMetrics.detailsTreeGaId, - ); - legacyInspectorController = legacy.InspectorController( - inspectorTree: legacyInspectorTreeController, - detailsTree: legacyDetailsTreeController, + inspectorController = InspectorController( + inspectorTree: inspectorTreeController, treeType: FlutterTreeType.widget, ); } @override void dispose() { - v2InspectorTreeController.dispose(); - v2InspectorController.dispose(); - - legacyInspectorTreeController.dispose(); - legacyDetailsTreeController.dispose(); - legacyInspectorController.dispose(); + inspectorTreeController.dispose(); + inspectorController.dispose(); super.dispose(); } } diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart index 823a3dd3c6a..a185822be0f 100644 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart +++ b/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart @@ -9,16 +9,11 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/material.dart'; import 'package:vm_service/vm_service.dart' hide Stack; -import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; -import '../../shared/feature_flags.dart'; import '../../shared/globals.dart'; -import '../../shared/managers/banner_messages.dart'; -import '../../shared/preferences/preferences.dart'; import '../../shared/primitives/simple_items.dart'; import '../../shared/ui/common_widgets.dart'; import '../../shared/ui/editable_list.dart'; -import 'inspector_screen.dart'; class FlutterInspectorSettingsDialog extends StatefulWidget { const FlutterInspectorSettingsDialog({super.key}); @@ -31,164 +26,71 @@ class FlutterInspectorSettingsDialog extends StatefulWidget { class _FlutterInspectorSettingsDialogState extends State with AutoDisposeMixin { - @override - void initState() { - super.initState(); - addAutoDisposeListener(preferences.inspector.legacyInspectorEnabled, () { - if (!preferences.inspector.legacyInspectorEnabled.value) { - bannerMessages.removeMessageByKey( - LegacyInspectorWarningMessage.buildKey(InspectorScreen.id), - InspectorScreen.id, - ); - } - }); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); const dialogHeight = 500.0; - return ValueListenableBuilder( - valueListenable: preferences.inspector.legacyInspectorEnabled, - builder: (context, legacyInspectorEnabled, _) { - final inspectorV2Enabled = !legacyInspectorEnabled; - return DevToolsDialog( - title: const DialogTitleText('Flutter Inspector Settings'), - content: SizedBox( - width: defaultDialogWidth, - height: dialogHeight, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + return DevToolsDialog( + title: const DialogTitleText('Flutter Inspector Settings'), + content: SizedBox( + width: defaultDialogWidth, + height: dialogHeight, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ...dialogSubHeader(theme, 'General'), + CheckboxSetting( + notifier: + preferences.inspector.hoverEvalModeEnabled + as ValueNotifier, + title: 'Enable hover inspection', + description: + 'Hovering over any widget displays its properties and values.', + gaItem: gac.inspectorHoverEvalMode, + ), + const SizedBox(height: largeSpacing), + CheckboxSetting( + notifier: + preferences.inspector.autoRefreshEnabled + as ValueNotifier, + title: 'Enable widget tree auto-refreshing', + description: + 'The widget tree will automatically refresh after a hot-reload or navigation event.', + gaItem: gac.inspectorAutoRefreshEnabled, + ), + const SizedBox(height: largeSpacing), + ...dialogSubHeader(theme, 'Package Directories'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - ...dialogSubHeader(theme, 'General'), - CheckboxSetting( - notifier: - preferences.inspector.hoverEvalModeEnabled - as ValueNotifier, - title: 'Enable hover inspection', - description: - 'Hovering over any widget displays its properties and values.', - gaItem: gac.inspectorHoverEvalMode, - ), - const SizedBox(height: largeSpacing), - if (inspectorV2Enabled) ...[ - CheckboxSetting( - notifier: - preferences.inspector.autoRefreshEnabled - as ValueNotifier, - title: 'Enable widget tree auto-refreshing', - description: - 'The widget tree will automatically refresh after a hot-reload or navigation event.', - gaItem: gac.inspectorAutoRefreshEnabled, - ), - ] else ...[ - const InspectorDefaultDetailsViewOption(), - ], - const SizedBox(height: largeSpacing), - // TODO(https://github.com/flutter/devtools/issues/7860): Clean-up - // after Inspector V2 has been released. - if (FeatureFlags.inspectorV2.isEnabled) - Flexible( - child: CheckboxSetting( - notifier: - preferences.inspector.legacyInspectorEnabled - as ValueNotifier, - title: 'Use legacy inspector', - description: - 'Disable the redesigned Flutter inspector. Please know that ' - 'the legacy inspector will be removed in a future release.', - gaItem: gac.inspectorV2Disabled, - ), + Expanded( + child: Text( + 'Widgets in these directories will show up in your summary tree.', + style: theme.subtleTextStyle, ), - const SizedBox(height: largeSpacing), - ...dialogSubHeader(theme, 'Package Directories'), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - 'Widgets in these directories will show up in your summary tree.', - style: theme.subtleTextStyle, - ), - ), - MoreInfoLink( - url: DocLinks.inspectorPackageDirectories.value, - gaScreenName: gac.inspector, - gaSelectedItemDescription: - gac.InspectorDocs.packageDirectoriesDocs.name, - ), - ], ), - Text( - '(e.g. /absolute/path/to/myPackage/)', - style: theme.subtleTextStyle, + MoreInfoLink( + url: DocLinks.inspectorPackageDirectories.value, + gaScreenName: gac.inspector, + gaSelectedItemDescription: + gac.InspectorDocs.packageDirectoriesDocs.name, ), - const SizedBox(height: denseSpacing), - const Expanded(child: PubRootDirectorySection()), ], ), - ), - actions: const [DialogCloseButton()], - ); - }, - ); - } -} - -class InspectorDefaultDetailsViewOption extends StatelessWidget { - const InspectorDefaultDetailsViewOption({super.key}); - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: preferences.inspector.defaultDetailsView, - builder: (context, selection, _) { - final theme = Theme.of(context); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ Text( - 'Select the default tab for the inspector.', + '(e.g. /absolute/path/to/myPackage/)', style: theme.subtleTextStyle, ), const SizedBox(height: denseSpacing), - RadioGroup( - groupValue: selection, - onChanged: _onChanged, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Radio( - value: InspectorDetailsViewType.layoutExplorer, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - Text(InspectorDetailsViewType.layoutExplorer.key), - const SizedBox(width: denseSpacing), - const Radio( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: InspectorDetailsViewType.widgetDetailsTree, - ), - Text(InspectorDetailsViewType.widgetDetailsTree.key), - ], - ), - ), + const Expanded(child: PubRootDirectorySection()), ], - ); - }, + ), + ), + actions: const [DialogCloseButton()], ); } - - void _onChanged(InspectorDetailsViewType? value) { - if (value != null) { - preferences.inspector.setDefaultInspectorDetailsView(value); - final item = value.name == InspectorDetailsViewType.layoutExplorer.name - ? gac.defaultDetailsViewToLayoutExplorer - : gac.defaultDetailsViewToWidgetDetails; - ga.select(gac.inspector, item); - } - } } class PubRootDirectorySection extends StatelessWidget { diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart index 188dabd14b6..1d166cedad5 100644 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_data_models.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. -/// @docImport '../inspector/layout_explorer/ui/overflow_indicator_painter.dart'; +/// @docImport '../inspector_v2/layout_explorer/ui/overflow_indicator_painter.dart'; library; import 'dart:math' as math; diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart index 5b4b1ec2d4f..859b6508cad 100644 --- a/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/layout_explorer/ui/utils.dart @@ -9,8 +9,8 @@ import 'package:flutter/material.dart'; import '../../../../shared/diagnostics/diagnostics_node.dart'; import '../../../../shared/primitives/utils.dart'; -import '../../../inspector/layout_explorer/ui/dimension.dart'; import '../../inspector_data_models.dart'; +import 'dimension.dart'; import 'overflow_indicator_painter.dart'; import 'theme.dart'; import 'widgets_theme.dart'; diff --git a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart index fa93986930c..93fd3c08ed6 100644 --- a/packages/devtools_app/lib/src/screens/logging/logging_controller.dart +++ b/packages/devtools_app/lib/src/screens/logging/logging_controller.dart @@ -27,7 +27,7 @@ import '../../shared/primitives/message_bus.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/ui/filter.dart'; import '../../shared/ui/search.dart'; -import '../inspector/inspector_tree_controller.dart'; +import '../inspector_v2/inspector_tree_controller.dart'; import 'log_details_controller.dart'; import 'logging_screen.dart'; import 'metadata.dart'; diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index 45450f842d9..aaf721b6fbb 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart @@ -8,7 +8,6 @@ library; import 'package:devtools_shared/devtools_extensions.dart'; import '../framework/screen.dart'; -import '../preferences/preferences.dart'; part 'constants/_cpu_profiler_constants.dart'; part 'constants/_debugger_constants.dart'; @@ -97,6 +96,9 @@ const onDeviceSelection = 'onDeviceSelection'; const inspectorSettings = 'inspectorSettings'; const loggingSettings = 'loggingSettings'; const refreshPubRoots = 'refreshPubRoots'; + +enum InspectorDetailsViewType { layoutExplorer, widgetDetailsTree } + final defaultDetailsViewToLayoutExplorer = InspectorDetailsViewType.layoutExplorer.name; final defaultDetailsViewToWidgetDetails = diff --git a/packages/devtools_app/lib/src/shared/analytics/metrics.dart b/packages/devtools_app/lib/src/shared/analytics/metrics.dart index fa0195a9021..a1be45f256e 100644 --- a/packages/devtools_app/lib/src/shared/analytics/metrics.dart +++ b/packages/devtools_app/lib/src/shared/analytics/metrics.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. -/// @docImport '../../screens/inspector/inspector_tree_controller.dart'; +/// @docImport '../../screens/inspector_v2/inspector_tree_controller.dart'; /// @docImport '../../screens/performance/panes/flutter_frames/flutter_frame_model.dart'; library; @@ -67,12 +67,6 @@ class ProfilerScreenMetrics extends ScreenAnalyticsMetrics { } class InspectorScreenMetrics extends ScreenAnalyticsMetrics { - InspectorScreenMetrics.legacy({ - this.rootSetCount, - this.rowCount, - this.inspectorTreeControllerId, - }) : isV2 = false; - InspectorScreenMetrics.v2({ this.rootSetCount, this.rowCount, diff --git a/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart b/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart deleted file mode 100644 index 1afbf488668..00000000000 --- a/packages/devtools_app/lib/src/shared/console/eval/inspector_tree.dart +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -/// Inspector specific tree rendering support. -/// -/// This library must not have direct dependencies on web-only libraries. -/// -/// This allows tests of the complicated logic in this class to run on the VM. -library; - -import 'package:flutter/foundation.dart'; - -import '../../diagnostics/diagnostics_node.dart'; -import '../../ui/search.dart'; - -/// Split text into two groups, word characters at the start of a string and all -/// other characters. -final treeNodePrimaryDescriptionPattern = RegExp(r'^([\w ]+)(.*)$'); -// TODO(jacobr): temporary workaround for missing structure from assertion thrown building -// widget errors. -final assertionThrownBuildingError = RegExp( - r'^(The following assertion was thrown building [a-zA-Z]+)(\(.*\))(:)$', -); - -typedef TreeEventCallback = void Function(InspectorTreeNode node); - -const iconPadding = 4.0; -const chartLineStrokeWidth = 1.0; -const inspectorColumnWidth = 12.0; -const inspectorRowHeight = 16.0; - -/// This class could be refactored out to be a reasonable generic collapsible -/// tree ui node class but we choose to instead make it widget inspector -/// specific as that is the only case we care about. -// TODO(kenz): extend TreeNode class to share tree logic. -class InspectorTreeNode { - InspectorTreeNode({InspectorTreeNode? parent, bool expandChildren = true}) - : _children = [], - _parent = parent, - _isExpanded = expandChildren; - - bool get showLinesToChildren { - return _children.length > 1 && !_children.last.isProperty; - } - - bool get isDirty => _isDirty; - bool _isDirty = true; - - set isDirty(bool dirty) { - if (dirty) { - _isDirty = true; - _shouldShow = null; - if (_childrenCount == null) { - // Already dirty. - return; - } - _childrenCount = null; - if (parent != null) { - parent!.isDirty = true; - } - } else { - _isDirty = false; - } - } - - /// Returns whether the node is currently visible in the tree. - void updateShouldShow(bool value) { - if (value != _shouldShow) { - _shouldShow = value; - for (final child in children) { - child.updateShouldShow(value); - } - } - } - - bool get shouldShow { - final parentLocal = parent; - _shouldShow ??= - parentLocal == null || parentLocal.isExpanded && parentLocal.shouldShow; - return _shouldShow!; - } - - bool? _shouldShow; - - bool selected = false; - - RemoteDiagnosticsNode? _diagnostic; - final List _children; - - Iterable get children => _children; - - bool get isProperty { - final diagnosticLocal = diagnostic; - return diagnosticLocal == null || diagnosticLocal.isProperty; - } - - bool get isExpanded => _isExpanded; - bool _isExpanded; - - bool allowExpandCollapse = true; - - bool get showExpandCollapse { - return (diagnostic?.hasChildren == true || children.isNotEmpty) && - allowExpandCollapse; - } - - set isExpanded(bool value) { - if (value != _isExpanded) { - _isExpanded = value; - isDirty = true; - if (_shouldShow ?? false) { - for (final child in children) { - child.updateShouldShow(value); - } - } - } - } - - InspectorTreeNode? get parent => _parent; - InspectorTreeNode? _parent; - - set parent(InspectorTreeNode? value) { - _parent = value; - _parent?.isDirty = true; - } - - RemoteDiagnosticsNode? get diagnostic => _diagnostic; - - set diagnostic(RemoteDiagnosticsNode? v) { - final value = v!; - _diagnostic = value; - _isExpanded = value.childrenReady; - isDirty = true; - } - - int get childrenCount { - if (!isExpanded) { - _childrenCount = 0; - } - final childrenCountLocal = _childrenCount; - if (childrenCountLocal != null) { - return childrenCountLocal; - } - int count = 0; - for (final child in _children) { - count += child.subtreeSize; - } - return _childrenCount = count; - } - - bool get hasPlaceholderChildren { - return children.length == 1 && children.first.diagnostic == null; - } - - int? _childrenCount; - - int get subtreeSize => childrenCount + 1; - - // TODO(jacobr): move getRowIndex to the InspectorTree class. - int getRowIndex(InspectorTreeNode node) { - int index = 0; - while (true) { - final parent = node.parent; - if (parent == null) { - break; - } - for (final sibling in parent._children) { - if (sibling == node) { - break; - } - index += sibling.subtreeSize; - } - index += 1; // For parent itself. - node = parent; - } - return index; - } - - // TODO(jacobr): move this method to the InspectorTree class. - // TODO: optimize this method. - InspectorTreeRow? getRow(int index) { - if (subtreeSize <= index) { - return null; - } - - final ticks = []; - InspectorTreeNode node = this; - int current = 0; - int depth = 0; - - // Iterate till getting the result to return. - while (true) { - final style = node.diagnostic?.style; - final indented = - style != DiagnosticsTreeStyle.flat && - style != DiagnosticsTreeStyle.error; - if (current == index) { - return InspectorTreeRow( - node: node, - index: index, - ticks: ticks, - depth: depth, - lineToParent: - !node.isProperty && - index != 0 && - node.parent!.showLinesToChildren, - ); - } - assert(index > current); - current++; - final children = node._children; - int i; - for (i = 0; i < children.length; ++i) { - final child = children[i]; - final subtreeSize = child.subtreeSize; - if (current + subtreeSize > index) { - node = child; - if (children.length > 1 && - i + 1 != children.length && - !children.last.isProperty) { - if (indented) { - ticks.add(depth); - } - } - break; - } - current += subtreeSize; - } - assert(i < children.length); - if (indented) { - depth++; - } - } - } - - void removeChild(InspectorTreeNode child) { - child.parent = null; - final removed = _children.remove(child); - assert(removed); - isDirty = true; - } - - void appendChild(InspectorTreeNode child) { - _children.add(child); - child.parent = this; - isDirty = true; - } - - void clearChildren() { - _children.clear(); - isDirty = true; - } -} - -/// A row in the tree with all information required to render it. -class InspectorTreeRow with SearchableDataMixin { - InspectorTreeRow({ - required this.node, - required this.index, - required this.ticks, - required this.depth, - required this.lineToParent, - }); - - final InspectorTreeNode node; - - /// Column indexes of ticks to draw lines from parents to children. - final List ticks; - final int depth; - final int index; - final bool lineToParent; - - bool get isSelected => node.selected; -} - -/// Callback issued every time a node is added to the tree. -typedef NodeAddedCallback = - void Function( - InspectorTreeNode node, - RemoteDiagnosticsNode diagnosticsNode, - ); - -class InspectorTreeConfig { - InspectorTreeConfig({ - this.onNodeAdded, - this.onClientActiveChange, - this.onSelectionChange, - this.onExpand, - }); - - final NodeAddedCallback? onNodeAdded; - final VoidCallback? onSelectionChange; - final void Function(bool added)? onClientActiveChange; - final TreeEventCallback? onExpand; -} - -enum SearchTargetType { - widget, - // TODO(https://github.com/flutter/devtools/issues/3489) implement other search scopes: details, all etc -} 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 7ea83892fd9..f7006f910b0 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/description.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/description.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. -/// @docImport '../../../screens/inspector/inspector_tree_controller.dart'; +/// @docImport '../../../screens/inspector_v2/inspector_tree_controller.dart'; library; import 'package:devtools_app_shared/ui.dart'; @@ -17,7 +17,7 @@ import '../../primitives/utils.dart'; import '../../ui/hover.dart'; import '../../ui/icons.dart'; import '../../ui/utils.dart'; -import '../eval/inspector_tree.dart'; +import '../eval/inspector_tree_v2.dart'; import 'expandable_variable.dart'; final _colorIconMaker = ColorIconMaker(); diff --git a/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart b/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart index 440c2cc3d1b..7efdbcbfeb7 100644 --- a/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences/_inspector_preferences.dart @@ -4,25 +4,10 @@ part of 'preferences.dart'; -enum InspectorDetailsViewType { - layoutExplorer(nameOverride: 'Layout Explorer'), - widgetDetailsTree(nameOverride: 'Widget Details Tree'); - - const InspectorDetailsViewType({String? nameOverride}) - : _nameOverride = nameOverride; - - final String? _nameOverride; - - String get key => _nameOverride ?? name; -} - class InspectorPreferencesController extends DisposableController with AutoDisposeControllerMixin { ValueListenable get hoverEvalModeEnabled => _hoverEvalMode; - ValueListenable get legacyInspectorEnabled => _legacyInspectorEnabled; ValueListenable get autoRefreshEnabled => _autoRefreshEnabled; - ValueListenable get defaultDetailsView => - _defaultDetailsView; ListValueNotifier get pubRootDirectories => _pubRootDirectories; ValueListenable get isRefreshingPubRootDirectories => _pubRootDirectoriesAreBusy; @@ -30,21 +15,13 @@ class InspectorPreferencesController extends DisposableController serviceConnection.inspectorService; final _hoverEvalMode = ValueNotifier(false); - final _legacyInspectorEnabled = ValueNotifier(false); final _autoRefreshEnabled = ValueNotifier(true); final _pubRootDirectories = ListValueNotifier([]); final _pubRootDirectoriesAreBusy = ValueNotifier(false); final _busyCounter = ValueNotifier(0); - final _defaultDetailsView = ValueNotifier( - InspectorDetailsViewType.layoutExplorer, - ); static const _hoverEvalModeStorageId = 'inspector.hoverEvalMode'; - static const _legacyInspectorEnabledStorageId = - 'inspector.legacyInspectorEnabled'; static const _autoRefreshEnabledStorageId = 'inspector.autoRefreshEnabled'; - static const _defaultDetailsViewStorageId = - 'inspector.defaultDetailsViewType'; static const _customPubRootDirectoriesStoragePrefix = 'inspector.customPubRootDirectories'; @@ -83,11 +60,8 @@ class InspectorPreferencesController extends DisposableController @override Future init() async { await _initHoverEvalMode(); - await _initLegacyInspectorEnabled(); await _initAutoRefreshEnabled(); - // TODO(jacobr): consider initializing this first as it is not blocking. _initPubRootDirectories(); - await _initDefaultInspectorDetailsView(); } Future _initHoverEvalMode() async { @@ -98,16 +72,6 @@ class InspectorPreferencesController extends DisposableController ); } - Future _initLegacyInspectorEnabled() async { - // TODO(https://github.com/flutter/devtools/issues/8667): Consider removing - // the old 'inspector.inspectorV2Enabled' key if it is set. - await _updateLegacyInspectorEnabled(); - _saveBooleanPreferenceChanges( - preferenceStorageId: _legacyInspectorEnabledStorageId, - preferenceNotifier: _legacyInspectorEnabled, - ); - } - Future _initAutoRefreshEnabled() async { await _updateAutoRefreshEnabled(); _saveBooleanPreferenceChanges( @@ -124,14 +88,6 @@ class InspectorPreferencesController extends DisposableController ); } - Future _updateLegacyInspectorEnabled() async { - await _updateBooleanPreference( - preferenceStorageId: _legacyInspectorEnabledStorageId, - preferenceNotifier: _legacyInspectorEnabled, - defaultValue: false, - ); - } - Future _updateAutoRefreshEnabled() async { await _updateBooleanPreference( preferenceStorageId: _autoRefreshEnabledStorageId, @@ -164,31 +120,6 @@ class InspectorPreferencesController extends DisposableController preferenceNotifier.value = preferenceValue == 'true'; } - Future _initDefaultInspectorDetailsView() async { - await _updateInspectorDetailsViewSelection(); - - addAutoDisposeListener(_defaultDetailsView, () { - safeUnawaited( - storage.setValue( - _defaultDetailsViewStorageId, - _defaultDetailsView.value.name.toString(), - ), - ); - }); - } - - Future _updateInspectorDetailsViewSelection() async { - final inspectorDetailsView = await storage.getValue( - _defaultDetailsViewStorageId, - ); - - if (inspectorDetailsView != null) { - _defaultDetailsView.value = InspectorDetailsViewType.values.firstWhere( - (e) => e.name.toString() == inspectorDetailsView, - ); - } - } - void _initPubRootDirectories() { addAutoDisposeListener( serviceConnection.serviceManager.connectedState, @@ -245,9 +176,7 @@ class InspectorPreferencesController extends DisposableController _checkedFlutterPubRoot = false; await _updateMainScriptRef(); await _updateHoverEvalMode(); - await _updateLegacyInspectorEnabled(); await loadPubRootDirectories(); - await _updateInspectorDetailsViewSelection(); } Future loadPubRootDirectories() async { @@ -473,17 +402,8 @@ class InspectorPreferencesController extends DisposableController _hoverEvalMode.value = enableHoverEvalMode; } - @visibleForTesting - void setLegacyInspectorEnabled(bool legacyInspectorEnabled) { - _legacyInspectorEnabled.value = legacyInspectorEnabled; - } - @visibleForTesting void setAutoRefreshEnabled(bool autoRefreshEnabled) { _autoRefreshEnabled.value = autoRefreshEnabled; } - - void setDefaultInspectorDetailsView(InspectorDetailsViewType value) { - _defaultDetailsView.value = value; - } } diff --git a/packages/devtools_app/lib/src/shared/ui/icons.dart b/packages/devtools_app/lib/src/shared/ui/icons.dart index 9feaa28877e..979cf15a989 100644 --- a/packages/devtools_app/lib/src/shared/ui/icons.dart +++ b/packages/devtools_app/lib/src/shared/ui/icons.dart @@ -13,7 +13,7 @@ library; import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/material.dart'; -import '../../screens/inspector/layout_explorer/ui/widgets_theme.dart'; +import '../../screens/inspector_v2/layout_explorer/ui/widgets_theme.dart'; import 'colors.dart'; class CustomIcon extends StatelessWidget { diff --git a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj index ce7a071de3b..bd41a2cd64a 100644 --- a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + A8DF12F92A961E0BE748392A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */; }; + FBB6B0E13D9A365390189BEF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -61,6 +63,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 275258A68FB221CA96512D9D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -77,9 +81,15 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 492AAD93062A49196993214B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + D36B797FD17AF803B3F3A442 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -87,6 +97,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A8DF12F92A961E0BE748392A /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -95,12 +106,22 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + FBB6B0E13D9A365390189BEF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 2451268784E41F8223F03594 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */, + A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -127,6 +148,8 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, + 59A677840E39A9BEE994C28D /* Pods */, + 2451268784E41F8223F03594 /* Frameworks */, ); sourceTree = ""; }; @@ -175,6 +198,20 @@ path = Runner; sourceTree = ""; }; + 59A677840E39A9BEE994C28D /* Pods */ = { + isa = PBXGroup; + children = ( + D36B797FD17AF803B3F3A442 /* Pods-Runner.debug.xcconfig */, + 275258A68FB221CA96512D9D /* Pods-Runner.release.xcconfig */, + 492AAD93062A49196993214B /* Pods-Runner.profile.xcconfig */, + 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */, + B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */, + BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -182,6 +219,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 81D9EC1F921BB673D2821177 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -200,6 +238,7 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 69D32B8BF13975CC87752C21 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, @@ -330,6 +369,50 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; + 69D32B8BF13975CC87752C21 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 81D9EC1F921BB673D2821177 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -381,6 +464,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -395,6 +479,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -409,6 +494,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/packages/devtools_app/test/screens/inspector/diagnostics_test.dart b/packages/devtools_app/test/screens/inspector/diagnostics_test.dart deleted file mode 100644 index 1e1bb3e2457..00000000000 --- a/packages/devtools_app/test/screens/inspector/diagnostics_test.dart +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2022 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:convert'; - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/shared/ui/utils.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('DiagnosticsNodeDescription', () { - final renderObjectJson = jsonDecode(''' - { - "properties": [ - { - "description": "horizontal", - "name": "direction" - }, - { - "description": "start", - "name": "mainAxisAlignment" - }, - { - "description": "max", - "name": "mainAxisSize" - }, - { - "description": "center", - "name": "crossAxisAlignment" - }, - { - "description": "ltr", - "name": "textDirection" - }, - { - "description": "down", - "name": "verticalDirection" - } - ] - } - '''); - setUp(() { - setGlobal( - DevToolsEnvironmentParameters, - ExternalDevToolsEnvironmentParameters(), - ); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(ServiceConnectionManager, FakeServiceConnectionManager()); - }); - - group('hover eval', () { - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - }; - final inspectorService = MockInspectorObjectGroupBase(); - final diagnostic = RemoteDiagnosticsNode( - nodeJson, - inspectorService, - false, - null, - ); - late DiagnosticsNodeDescription diagnosticsNodeDescription; - - setUp(() { - preferences.inspector.setHoverEvalMode(true); - diagnosticsNodeDescription = DiagnosticsNodeDescription(diagnostic); - }); - - testWidgets('can be enabled from preferences', ( - WidgetTester tester, - ) async { - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final hoverCardTooltip = - tester.widget(find.byType(HoverCardTooltip)) as HoverCardTooltip; - expect(hoverCardTooltip.enabled(), true); - }); - - testWidgets('can be disabled from preferences', ( - WidgetTester tester, - ) async { - preferences.inspector.setHoverEvalMode(false); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final hoverCardTooltip = - tester.widget(find.byType(HoverCardTooltip)) as HoverCardTooltip; - expect(hoverCardTooltip.enabled(), false); - }); - - testWidgets('disabled when inspector service not set', ( - WidgetTester tester, - ) async { - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - false, - null, - ); - diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final hoverCardTooltip = - tester.widget(find.byType(HoverCardTooltip)) as HoverCardTooltip; - expect(hoverCardTooltip.enabled(), false); - }); - }); - - group('approximateNodeWidth', () { - const epsilon = 7.0; - testWidgets('property diagnostics node with name and description', ( - WidgetTester tester, - ) async { - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - 'description': - 'this is a showname description, which will show up after the name', - 'showName': true, - 'name': 'THE NAME to be shown', - }; - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - true, - null, - ); - final diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final approximatedWidth = - DiagnosticsNodeDescription.approximateNodeWidth( - diagnosticWithoutService, - ); - - final diagnosticsNodeFind = find.byType(DiagnosticsNodeDescription); - // There are many rich texts, containg the name, and description. - final allRichTexts = find - .descendant( - of: diagnosticsNodeFind, - matching: find.byType(RichText), - ) - .evaluate() - .map((e) => e.widget as RichText); - final measuredWidthOfAllRichTexts = allRichTexts.fold( - 0, - (previousValue, richText) => - previousValue + calculateTextSpanWidth(richText.text as TextSpan), - ); - expect( - approximatedWidth, - moreOrLessEquals(measuredWidthOfAllRichTexts, epsilon: epsilon), - ); - }); - - testWidgets('diagnostics node with icon and description', ( - WidgetTester tester, - ) async { - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'description': 'This is the description', - 'children': [], - 'showName': false, - }; - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - false, - null, - ); - final diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final approximatedTextWidth = - DiagnosticsNodeDescription.approximateNodeWidth( - diagnosticWithoutService, - ); - - final diagnosticsNodeFind = find.byType(DiagnosticsNodeDescription); - // The icon is part of the clickable width, so we include it. - final measuredIconWidth = tester - .getSize( - find.descendant( - of: diagnosticsNodeFind, - matching: find.byType(AssetImageIcon), - ), - ) - .width; - - // There is only one rich text widget, containing the description. - final richTextWidget = - find - .descendant( - of: diagnosticsNodeFind, - matching: find.byType(RichText), - ) - .first - .evaluate() - .first - .widget - as RichText; - final measuredTextWidth = calculateTextSpanWidth( - richTextWidget.text as TextSpan, - ); - - expect( - approximatedTextWidth, - moreOrLessEquals( - measuredTextWidth + measuredIconWidth, - epsilon: epsilon, - ), - ); - }); - - testWidgets('error node with different fontSize', ( - WidgetTester tester, - ) async { - // Nodes with normal levels default to using the default fontSize, so - // using an error level node allows us to test different font sizes. - final nodeJson = { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - 'description': - 'this is a showname description, which will show up after the name', - 'showName': true, - 'name': 'THE NAME to be shown', - 'level': 'error', - }; - final diagnosticWithoutService = RemoteDiagnosticsNode( - nodeJson, - null, - false, - null, - ); - - //Use a textStyle that is much larger than the normal style - const textStyle = TextStyle(fontSize: 24.0, fontFamily: 'Roboto'); - final diagnosticsNodeDescription = DiagnosticsNodeDescription( - diagnosticWithoutService, - style: textStyle, - ); - - await tester.pumpWidget(wrap(diagnosticsNodeDescription)); - - final approximatedWidth = - DiagnosticsNodeDescription.approximateNodeWidth( - diagnosticWithoutService, - ); - - final diagnosticsNodeFind = find.byType(DiagnosticsNodeDescription); - // There are many rich texts, containg the name, and description. - final allRichTexts = find - .descendant( - of: diagnosticsNodeFind, - matching: find.byType(RichText), - ) - .evaluate() - .map((e) => e.widget as RichText); - - final measuredWidthOfAllRichTexts = allRichTexts.fold(0, ( - previousValue, - richText, - ) { - final originalTextSpan = richText.text as TextSpan; - - return previousValue + calculateTextSpanWidth(originalTextSpan); - }); - - expect( - approximatedWidth, - moreOrLessEquals(measuredWidthOfAllRichTexts, epsilon: epsilon), - ); - }); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart b/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart deleted file mode 100644 index b546faae833..00000000000 --- a/packages/devtools_app/test/screens/inspector/inspector_error_navigator_test.dart +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:collection'; - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - setUp(() { - setGlobal(ServiceConnectionManager, FakeServiceConnectionManager()); - setGlobal(IdeTheme, IdeTheme()); - }); - - group('Inspector Error Navigator', () { - Future testNavigate( - WidgetTester tester, { - required IconData tapIcon, - required int errorCount, - int? startIndex, - int? expectedIndex, - }) async { - var index = startIndex; - final navigator = ErrorNavigator( - errorIndex: index, - errors: _generateErrors(errorCount), - onSelectError: (newIndex) => index = newIndex, - ); - - await tester.pumpWidget(wrap(navigator)); - await tester.tap(find.byIcon(tapIcon)); - - expect(index, equals(expectedIndex)); - } - - testWidgets('shows count when no selection', (WidgetTester tester) async { - await tester.pumpWidget( - wrap( - ErrorNavigator( - errorIndex: null, - errors: _generateErrors(10), - onSelectError: (_) {}, - ), - ), - ); - expect(find.text('Errors: 10'), findsOneWidget); - }); - - testWidgets('shows x/y when selected error', (WidgetTester tester) async { - await tester.pumpWidget( - wrap( - ErrorNavigator( - errorIndex: 0, - errors: _generateErrors(10), - onSelectError: (_) {}, - ), - ), - ); - expect(find.text('Error 1/10'), findsOneWidget); - }); - - testWidgets( - 'can navigate forwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_down, - errorCount: 10, - startIndex: 5, - expectedIndex: 6, - ), - ); - - testWidgets( - 'can navigate backwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_up, - errorCount: 10, - startIndex: 5, - expectedIndex: 4, - ), - ); - - testWidgets( - 'wraps forwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_down, - errorCount: 10, - startIndex: 9, - expectedIndex: 0, - ), - ); - - testWidgets( - 'wraps backwards', - // Intentionally unawaited. - // ignore: discarded_futures - (WidgetTester tester) => testNavigate( - tester, - tapIcon: Icons.keyboard_arrow_up, - errorCount: 10, - startIndex: 0, - expectedIndex: 9, - ), - ); - }); -} - -LinkedHashMap _generateErrors(int count) => - LinkedHashMap.fromEntries( - List.generate( - count, - (index) => MapEntry( - 'error-$index', - InspectableWidgetError('Error $index', 'error-$index'), - ), - ), - ); diff --git a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart deleted file mode 100644 index 659959add0c..00000000000 --- a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright 2020 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../test_infra/flutter_test_driver.dart' show FlutterRunConfiguration; -import '../../test_infra/flutter_test_environment.dart'; -import '../../test_infra/matchers/matchers.dart'; - -// This is a bit conservative to ensure we do not get flakes due to -// slow interactions with the VM Service. This delay could likely be -// reduced to under 1 second without introducing flakes. -const inspectorChangeSettleTime = Duration(seconds: 2); - -void main() { - const windowSize = Size(2600.0, 1200.0); - // We need to use real async in this test so we need to use this binding. - initializeLiveTestWidgetsFlutterBindingWithAssets(); - - late FlutterTestEnvironment env; - - Future resetInspectorSelection() async { - final service = serviceConnection.inspectorService; - if (env.reuseTestEnvironment) { - // Ensure the previous test did not set the selection on the device. - // TODO(jacobr): add a proper method to WidgetInspectorService that does - // this. setSelection currently ignores null selection requests which is - // a misfeature. - await service!.inspectorLibrary.eval( - 'WidgetInspectorService.instance.selection.clear()', - isAlive: null, - ); - } - } - - setUp(() async { - await env.setupEnvironment(); - setGlobal(BannerMessagesController, BannerMessagesController()); - // Ensure the legacy inspector is enabled: - preferences.inspector.setLegacyInspectorEnabled(true); - }); - - group('screenshot tests', () { - setUpAll(() { - env = FlutterTestEnvironment( - const FlutterRunConfiguration(withDebugger: true), - ); - env.afterEverySetup = resetInspectorSelection; - }); - - tearDownAll(() async { - await env.tearDownEnvironment(force: true); - }); - - testWidgetsWithWindowSize('navigation', windowSize, ( - WidgetTester tester, - ) async { - await env.setupEnvironment(); - expect(serviceConnection.serviceManager.service, equals(env.service)); - expect(serviceConnection.serviceManager.isolateManager, isNotNull); - - final screen = InspectorScreen(); - await tester.pumpWidget( - wrapWithInspectorControllers(Builder(builder: screen.build)), - ); - await tester.pump(const Duration(seconds: 1)); - final InspectorScreenBodyState state = tester.state( - find.byType(InspectorScreenBody), - ); - final controller = state.controller; - while (!controller.flutterAppFrameReady) { - await controller.maybeLoadUI(); - await tester.pumpAndSettle(); - } - // Give time for the initial animation to complete. - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_initial_load.png', - ), - ); - - // Click on the Center widget (row index #5) - await tester.tap(find.richText('Center')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_select_center.png', - ), - ); - - // Select the details tree. - await tester.tap( - find.text(InspectorDetailsViewType.widgetDetailsTree.key), - ); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_select_center_details_tree.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - - // Select the RichText row. - await tester.tap(find.richText('RichText')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_richtext_selected.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - - // Test hovering over the icon shown when a property has its default - // value. - // TODO(jacobr): support tooltips in the Flutter version of the inspector. - // https://github.com/flutter/devtools/issues/2570. - // For example, verify that the tooltip hovering over the default value - // icons is "Default value". - // Test selecting a widget. - - // Two 'Scaffold's: a breadcrumb and an actual tree item - expect(find.richText('Scaffold'), findsNWidgets(2)); - // select Scaffold widget in summary tree. - await tester.tap(find.richText('Scaffold').last); - await tester.pumpAndSettle(inspectorChangeSettleTime); - // This tree is huge. If there is a change to package:flutter it may - // change. If this happens don't panic and rebaseline the golden. - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_scaffold_selected.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - - // The important thing about this is that the details tree should scroll - // instead of re-rooting as the selected row is already visible in the - // details tree. - await tester.tap(find.richText('AnimatedPhysicalModel')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_animated_physical_model_selected.png', - ), - // Implementation widgets from Flutter framework are not guaranteed to - // be stable. - skip: 'https://github.com/flutter/flutter/issues/172037', - ); - - await env.tearDownEnvironment(); - }); - - // TODO(jacobr): convert these tests to screenshot tests like the initial - // state test. - /* - - - // Intentionally trigger multiple quick navigate action to ensure that - // multiple quick navigation commands in a row do not trigger race - // conditions getting out of order updates from the server. - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - await detailsTree.nextUiFrame; - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar <-- selected\n' - ' [/icons/inspector/textArea.png]Text\n', - ), - ); - // Make sure we don't go off the bottom of the tree. - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - tree.navigateDown(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' [/icons/inspector/textArea.png]Text <-- selected\n', - ), - ); - tree.navigateUp(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar <-- selected\n' - ' [/icons/inspector/textArea.png]Text\n', - ), - ); - tree.navigateLeft(); - await detailsTree.nextUiFrame; - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar <-- selected\n', - ), - ); - tree.navigateLeft(); - // First navigate left goes to the parent. - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold <-- selected\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar\n', - ), - ); - tree.navigateLeft(); - // Next navigate left closes the parent. - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▶[S]Scaffold <-- selected\n', - ), - ); - - tree.navigateRight(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold <-- selected\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar\n', - ), - ); - - // Node is already expanded so this is equivalent to navigate down. - tree.navigateRight(); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center <-- selected\n' - ' │ [/icons/inspector/textArea.png]Text\n' - ' └─▶[A]AppBar\n', - ), - ); - - await detailsTree.nextUiFrame; - - // Make sure the details and main trees have not gotten out of sync. - expect( - detailsTree.toStringDeep(hidePropertyLines: true), - equalsIgnoringHashCodes('▼[C]Center <-- selected\n' - '└─▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[/icons/inspector/textArea.png]RichText\n'), - ); - - await env.tearDownEnvironment(); - }); - */ - - // TODO(jacobr): uncomment hotReload test once the hot reload test is not - // flaky. https://github.com/flutter/devtools/issues/642 - /* - test('hotReload', () async { - if (flutterVersion == '1.2.1') { - // This test can be flaky in Flutter 1.2.1 because of - // https://github.com/dart-lang/sdk/issues/33838 - // so we just skip it. This block of code can be removed after the next - // stable flutter release. - // TODO(dantup): Remove this. - return; - } - await env.setupEnvironment(); - - await serviceManager.performHotReload(); - // Ensure the inspector does not fall over and die after a hot reload. - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ [/icons/inspector/textArea.png]Text <-- selected\n' - ' └─▼[A]AppBar\n' - ' [/icons/inspector/textArea.png]Text\n', - ), - ); - - // TODO(jacobr): would be nice to have some tests that trigger a hot - // reload that actually changes app state in a meaningful way. - - await env.tearDownEnvironment(); - }); - */ - // TODO(jacobr): uncomment out the hotRestart tests once - // https://github.com/flutter/devtools/issues/337 is fixed. - /* - test('hotRestart', () async { - await env.setupEnvironment(); - - // The important thing about this is that the details tree should scroll - // instead of re-rooting as the selected row is already visible in the - // details tree. - simulateRowClick(tree, rowIndex: 4); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R]root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center <-- selected\n' - ' │ ▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' ▼[/icons/inspector/textArea.png]Text\n', - ), - ); - - /// After the hot restart some existing calls to the vm service may - /// timeout and that is ok. - serviceManager.manager.service.doNotWaitForPendingFuturesBeforeExit(); - - await serviceManager.performHotRestart(); - // The isolate starts out paused on a hot restart so we have to resume - // it manually to make the test pass. - - await serviceManager.manager.service - .resume(serviceManager.isolateManager.selectedIsolate.id); - - // First UI transition is to an empty tree. - await detailsTree.nextUiFrame; - expect(tree.toStringDeep(), equalsIgnoringHashCodes('\n')); - - // Notice that the selection has been lost due to the hot restart. - await detailsTree.nextUiFrame; - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center\n' - ' │ ▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' ▼[/icons/inspector/textArea.png]Text\n', - ), - ); - - // Verify that the selection can actually be changed after a restart. - simulateRowClick(tree, rowIndex: 4); - expect( - tree.toStringDeep(), - equalsIgnoringHashCodes( - '▼[R][root]\n' - ' ▼[M]MyApp\n' - ' ▼[M]MaterialApp\n' - ' ▼[S]Scaffold\n' - ' ├───▼[C]Center <-- selected\n' - ' │ ▼[/icons/inspector/textArea.png]Text\n' - ' └─▼[A]AppBar\n' - ' ▼[/icons/inspector/textArea.png]Text\n', - ), - ); - await env.tearDownEnvironment(); - }); -*/ - }); - - group('widget errors', () { - setUpAll(() async { - env = FlutterTestEnvironment( - testAppDirectory: 'test/test_infra/fixtures/inspector_app', - const FlutterRunConfiguration(withDebugger: true), - ); - await env.setupEnvironment( - config: const FlutterRunConfiguration( - withDebugger: true, - entryScript: 'lib/overflow_errors.dart', - ), - ); - env.afterEverySetup = resetInspectorSelection; - // Enable the legacy inspector. - preferences.inspector.setLegacyInspectorEnabled(true); - }); - - testWidgetsWithWindowSize('show navigator and error labels', windowSize, ( - WidgetTester tester, - ) async { - expect(serviceConnection.serviceManager.service, equals(env.service)); - expect(serviceConnection.serviceManager.isolateManager, isNotNull); - - final screen = InspectorScreen(); - await tester.pumpWidget( - wrapWithInspectorControllers(Builder(builder: screen.build)), - ); - await tester.pumpAndSettle(const Duration(seconds: 1)); - final InspectorScreenBodyState state = tester.state( - find.byType(InspectorScreenBody), - ); - final controller = state.controller; - while (!controller.flutterAppFrameReady) { - await controller.maybeLoadUI(); - await tester.pumpAndSettle(); - } - await env.flutter!.hotReload(); - // Give time for the initial animation to complete. - await tester.pumpAndSettle(inspectorChangeSettleTime); - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_errors_1_initial_load.png', - ), - ); - - // Navigate so one of the errors is selected. - for (var i = 0; i < 2; i++) { - await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); - await tester.pumpAndSettle(inspectorChangeSettleTime); - } - await expectLater( - find.byType(InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_errors_2_error_selected.png', - ), - ); - - await env.tearDownEnvironment(); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart deleted file mode 100644 index 0a09554f33e..00000000000 --- a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -// Fake construction requires number of unawaited calls. -// ignore_for_file: discarded_futures - -import 'dart:convert'; - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/screens/inspector/layout_explorer/flex/flex.dart'; -import 'package:devtools_app/src/screens/inspector/layout_explorer/layout_explorer.dart'; -import 'package:devtools_app/src/screens/inspector_shared/inspector_settings_dialog.dart'; -import 'package:devtools_app/src/service/service_extensions.dart' as extensions; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart' hide Fake; -import 'package:mockito/mockito.dart'; - -import '../../test_infra/flutter_test_storage.dart'; - -void main() { - final screen = InspectorScreen(); - - late FakeServiceConnectionManager fakeServiceConnection; - late FakeServiceExtensionManager fakeExtensionManager; - const windowSize = Size(2600.0, 1200.0); - - final debuggerController = createMockDebuggerControllerWithDefaults(); - - Widget buildInspectorScreen() { - return wrapWithControllers( - Builder(builder: screen.build), - debugger: debuggerController, - inspector: InspectorScreenController(), - ); - } - - setUp(() { - fakeServiceConnection = FakeServiceConnectionManager(); - fakeExtensionManager = - fakeServiceConnection.serviceManager.serviceExtensionManager; - mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!); - when( - fakeServiceConnection.errorBadgeManager.errorCountNotifier('inspector'), - ).thenReturn(ValueNotifier(0)); - - setGlobal( - DevToolsEnvironmentParameters, - ExternalDevToolsEnvironmentParameters(), - ); - setGlobal(ServiceConnectionManager, fakeServiceConnection); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(Storage, FlutterTestStorage()); - setGlobal(NotificationService, NotificationService()); - setGlobal(BannerMessagesController, BannerMessagesController()); - fakeServiceConnection.consoleService.ensureServiceInitialized(); - // Enable the legacy inspector: - preferences.inspector.setLegacyInspectorEnabled(true); - }); - - Future mockExtensions() async { - fakeExtensionManager.extensionValueOnDevice = { - extensions.toggleSelectWidgetMode.extension: true, - extensions.enableOnDeviceInspector.extension: true, - extensions.toggleOnDeviceWidgetInspector.extension: true, - extensions.debugPaint.extension: false, - }; - await fakeExtensionManager.fakeAddServiceExtension( - extensions.toggleOnDeviceWidgetInspector.extension, - ); - await fakeExtensionManager.fakeAddServiceExtension( - extensions.toggleSelectWidgetMode.extension, - ); - await fakeExtensionManager.fakeAddServiceExtension( - extensions.enableOnDeviceInspector.extension, - ); - await fakeExtensionManager.fakeAddServiceExtension( - extensions.debugPaint.extension, - ); - await fakeExtensionManager.fakeFrame(); - } - - void mockNoExtensionsAvailable() { - fakeExtensionManager.extensionValueOnDevice = { - extensions.toggleOnDeviceWidgetInspector.extension: true, - extensions.toggleSelectWidgetMode.extension: false, - extensions.debugPaint.extension: false, - }; - // Don't actually send any events to the client indicating that service - // extensions are avaiable. - fakeExtensionManager.fakeFrame(); - } - - testWidgetsWithWindowSize('builds its tab', windowSize, ( - WidgetTester tester, - ) async { - await tester.pumpWidget(buildInspectorScreen()); - await tester.pumpAndSettle(); - expect(find.byType(InspectorScreenBody), findsOneWidget); - }); - - group('Widget Errors', () { - // Display of error navigator/indicators is tested by a golden in - // inspector_integration_test.dart - - testWidgetsWithWindowSize( - 'does not render error navigator if no errors', - windowSize, - (WidgetTester tester) async { - await tester.pumpWidget(buildInspectorScreen()); - expect(find.byType(ErrorNavigator), findsNothing); - }, - ); - }); - - testWidgetsWithWindowSize('builds with no data', windowSize, ( - WidgetTester tester, - ) async { - // Make sure the window is wide enough to display description text. - - await tester.pumpWidget(buildInspectorScreen()); - expect(find.byType(InspectorScreenBody), findsOneWidget); - expect(find.byTooltip('Refresh Tree'), findsOneWidget); - expect(find.text(extensions.debugPaint.title), findsOneWidget); - // Make sure there is not an overflow if the window is narrow. - // TODO(jacobr): determine why there are overflows in the test environment - // but not on the actual device for this cae. - // await setWindowSize(const Size(1000.0, 1200.0)); - // Verify that description text is no-longer shown. - // expect(find.text(extensions.debugPaint.description), findsOneWidget); - }); - - testWidgetsWithWindowSize( - 'Test toggling service extension buttons', - windowSize, - (WidgetTester tester) async { - await mockExtensions(); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isFalse, - ); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - - await tester.pumpWidget(buildInspectorScreen()); - - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleSelectWidgetMode - .extension], - isTrue, - ); - - // We need a frame to find out that the service extension state has changed. - expect(find.byType(InspectorScreenBody), findsOneWidget); - expect( - find.text(extensions.toggleSelectWidgetMode.title), - findsOneWidget, - ); - expect(find.text(extensions.debugPaint.title), findsOneWidget); - await tester.pump(); - await tester.tap(find.text(extensions.toggleSelectWidgetMode.title)); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleSelectWidgetMode - .extension], - isFalse, - ); - // Verify the other service extension's state hasn't changed. - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isFalse, - ); - - await tester.tap(find.text(extensions.toggleSelectWidgetMode.title)); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleSelectWidgetMode - .extension], - isTrue, - ); - - await tester.tap(find.text(extensions.debugPaint.title)); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isTrue, - ); - }, - ); - - testWidgetsWithWindowSize( - 'Test toggling service extension buttons with no extensions available', - windowSize, - (WidgetTester tester) async { - mockNoExtensionsAvailable(); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .debugPaint - .extension], - isFalse, - ); - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - - await tester.pumpWidget(buildInspectorScreen()); - await tester.pump(); - expect(find.byType(InspectorScreenBody), findsOneWidget); - expect( - find.text(extensions.toggleOnDeviceWidgetInspector.title), - findsOneWidget, - ); - expect(find.text(extensions.debugPaint.title), findsOneWidget); - await tester.pump(); - - await tester.tap( - find.text(extensions.toggleOnDeviceWidgetInspector.title), - ); - // Verify the service extension state has not changed. - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - await tester.tap( - find.text(extensions.toggleOnDeviceWidgetInspector.title), - ); - // Verify the service extension state has not changed. - expect( - fakeExtensionManager.extensionValueOnDevice[extensions - .toggleOnDeviceWidgetInspector - .extension], - isTrue, - ); - - // TODO(jacobr): also verify that the service extension buttons look - // visually disabled. - }, - ); - - group('LayoutDetailsTab', () { - final renderObjectJson = jsonDecode(''' - { - "properties": [ - { - "description": "horizontal", - "name": "direction" - }, - { - "description": "start", - "name": "mainAxisAlignment" - }, - { - "description": "max", - "name": "mainAxisSize" - }, - { - "description": "center", - "name": "crossAxisAlignment" - }, - { - "description": "ltr", - "name": "textDirection" - }, - { - "description": "down", - "name": "verticalDirection" - } - ] - } - '''); - final diagnostic = RemoteDiagnosticsNode( - { - 'widgetRuntimeType': 'Row', - 'renderObject': renderObjectJson, - 'hasChildren': false, - 'children': [], - }, - null, - false, - null, - ); - final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - testWidgetsWithWindowSize( - 'should render StoryOfYourFlexWidget', - windowSize, - (WidgetTester tester) async { - final controller = TestInspectorController()..setSelectedNode(treeNode); - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: LayoutExplorerTab(controller: controller)), - ), - ); - expect(find.byType(FlexLayoutExplorerWidget), findsOneWidget); - }, - ); - - testWidgetsWithWindowSize( - 'should listen to controller selection event', - windowSize, - (WidgetTester tester) async { - final controller = TestInspectorController(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold(body: LayoutExplorerTab(controller: controller)), - ), - ); - expect(find.byType(FlexLayoutExplorerWidget), findsNothing); - controller.setSelectedNode(treeNode); - await tester.pumpAndSettle(); - expect(find.byType(FlexLayoutExplorerWidget), findsOneWidget); - }, - ); - }); - - group('FlutterInspectorSettingsDialog', () { - const startingHoverEvalModeValue = false; - - setUp(() { - preferences.inspector.setHoverEvalMode(startingHoverEvalModeValue); - }); - - testWidgetsWithWindowSize( - 'can update hover inspection setting', - windowSize, - (WidgetTester tester) async { - await tester.pumpWidget(buildInspectorScreen()); - - await tester.tap(find.byType(SettingsOutlinedButton)); - await tester.pumpAndSettle(); - expect(find.byType(FlutterInspectorSettingsDialog), findsOneWidget); - - final hoverCheckBoxSetting = find.ancestor( - of: find.richTextContaining('Enable hover inspection'), - matching: find.byType(CheckboxSetting), - ); - final hoverModeCheckBox = find.descendant( - of: hoverCheckBoxSetting, - matching: find.byType(NotifierCheckbox), - ); - await tester.tap(hoverModeCheckBox); - await tester.pumpAndSettle(); - expect( - preferences.inspector.hoverEvalModeEnabled.value, - !startingHoverEvalModeValue, - ); - }, - ); - }); - - // TODO(jacobr): add screenshot tests that connect to a test application - // in the same way the inspector_controller test does today and take golden - // images. Alternately: support an offline inspector mode and add tests of - // that mode which would enable faster tests that run as unittests. -} diff --git a/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart deleted file mode 100644 index 17d1b1238a3..00000000000 --- a/packages/devtools_app/test/screens/inspector/inspector_tree_test.dart +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/screens/inspector/inspector_breadcrumbs.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart' hide Fake; -import 'package:mockito/mockito.dart'; - -import 'utils/inspector_tree.dart'; - -void main() { - late FakeServiceConnectionManager fakeServiceConnection; - late InspectorController inspectorController; - - setUp(() { - fakeServiceConnection = FakeServiceConnectionManager(); - final app = fakeServiceConnection.serviceManager.connectedApp!; - when(app.isFlutterAppNow).thenReturn(true); - when(app.isProfileBuildNow).thenReturn(false); - - setGlobal( - DevToolsEnvironmentParameters, - ExternalDevToolsEnvironmentParameters(), - ); - setGlobal(ServiceConnectionManager, fakeServiceConnection); - setGlobal(IdeTheme, IdeTheme()); - setGlobal(PreferencesController, PreferencesController()); - setGlobal(NotificationService, NotificationService()); - setGlobal(BreakpointManager, BreakpointManager()); - mockConnectedApp(fakeServiceConnection.serviceManager.connectedApp!); - - inspectorController = InspectorController( - inspectorTree: InspectorTreeController(), - detailsTree: InspectorTreeController(), - treeType: FlutterTreeType.widget, - )..firstInspectorTreeLoadCompleted = true; - }); - - Future pumpInspectorTree( - WidgetTester tester, { - required InspectorTreeController treeController, - bool isSummaryTree = false, - }) async { - final debuggerController = DebuggerController(); - final summaryTreeController = isSummaryTree - ? null - : InspectorTreeController(); - await tester.pumpWidget( - wrapWithControllers( - debugger: debuggerController, - InspectorTree( - controller: inspectorController, - treeController: treeController, - summaryTreeController: summaryTreeController, - isSummaryTree: isSummaryTree, - ), - ), - ); - await tester.pumpAndSettle(); - } - - group('InspectorTreeController', () { - testWidgets('Row with negative index regression test', ( - WidgetTester tester, - ) async { - final treeController = InspectorTreeController() - ..config = InspectorTreeConfig( - onNodeAdded: (_, _) {}, - onClientActiveChange: (_) {}, - ); - await pumpInspectorTree(tester, treeController: treeController); - - expect(treeController.getRow(const Offset(0, -100.0)), isNull); - expect(treeController.getRowOffset(-1), equals(0)); - - expect(treeController.getRow(const Offset(0, 0.0)), isNull); - expect(treeController.getRowOffset(0), equals(0)); - - treeController.root = InspectorTreeNode() - ..appendChild(InspectorTreeNode()); - - await pumpInspectorTree(tester, treeController: treeController); - - expect(treeController.getRow(const Offset(0, -20))!.index, 0); - expect(treeController.getRowOffset(-1), equals(0)); - expect(treeController.getRow(const Offset(0, 0.0)), isNotNull); - expect(treeController.getRowOffset(0), equals(0)); - - // This operation would previously throw an exception in debug builds - // and infinite loop in release builds. - treeController.scrollToRect(const Rect.fromLTWH(0, -20, 100, 100)); - }); - }); - - group('Inspector tree content preview', () { - testWidgets('Shows simple text preview', (WidgetTester tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Content'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.richText('Text: "Content"'), findsOneWidget); - }); - - testWidgets('Shows preview from Text.rich', (WidgetTester tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text.rich( - TextSpan( - children: [ - TextSpan(text: 'Rich '), - TextSpan(text: 'text'), - ], - ), - ), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.richText('Text: "Rich text"'), findsOneWidget); - }); - - testWidgets('Strips new lines from text preview', ( - WidgetTester tester, - ) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Multiline\ntext\n\ncontent'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.richText('Text: "Multiline text content"'), findsOneWidget); - }); - - testWidgets('Shows breadcrumbs in Widget detail tree', (tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Hello'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree(tester, treeController: treeController); - - expect(find.byType(InspectorBreadcrumbNavigator), findsOneWidget); - }); - - testWidgets('Shows no breadcrumbs widget in summary tree', (tester) async { - final diagnosticNode = await widgetToInspectorTreeDiagnosticsNode( - widget: const Text('Hello'), - tester: tester, - ); - - final treeController = inspectorTreeControllerFromNode(diagnosticNode); - await pumpInspectorTree( - tester, - treeController: treeController, - isSummaryTree: true, - ); - - expect(find.byType(InspectorBreadcrumbNavigator), findsNothing); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/flex/arrow_test.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/flex/arrow_test.dart deleted file mode 100644 index 2b5bf8cf887..00000000000 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/flex/arrow_test.dart +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/src/screens/inspector/layout_explorer/ui/arrow.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../../../test_infra/matchers/matchers.dart'; - -void main() { - const relativeGoldenPath = - '../../../../test_infra/goldens/inspector/layout_explorer/flex'; - - group('Arrow Golden Tests', () { - group('Unidirectional', () { - Widget buildUnidirectionalArrowWrapper(ArrowType type) => Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 100, - height: 100, - child: ArrowWrapper.unidirectional( - type: type, - arrowColor: Colors.black, - arrowHeadSize: 8.0, - child: Container(width: 10, height: 10, color: Colors.red), - ), - ), - ); - testWidgets('left', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.left); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_left.png', - ), - ); - }, skip: kIsWeb); - testWidgets('up', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.up); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_up.png', - ), - ); - }, skip: kIsWeb); - testWidgets('right', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.right); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_right.png', - ), - ); - }, skip: kIsWeb); - testWidgets('down', (WidgetTester tester) async { - final widget = buildUnidirectionalArrowWrapper(ArrowType.down); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_unidirectional_down.png', - ), - ); - }, skip: kIsWeb); - }); - - group('Bidirectional', () { - Widget buildBidirectionalArrowWrapper(Axis direction) => Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 100, - height: 100, - child: ArrowWrapper.bidirectional( - direction: direction, - arrowColor: Colors.black, - arrowHeadSize: 8.0, - child: Container(width: 10, height: 10, color: Colors.red), - ), - ), - ); - testWidgets('horizontal', (WidgetTester tester) async { - final widget = buildBidirectionalArrowWrapper(Axis.horizontal); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_bidirectional_horizontal.png', - ), - ); - }, skip: kIsWeb); - testWidgets('vertical', (WidgetTester tester) async { - final widget = buildBidirectionalArrowWrapper(Axis.vertical); - await tester.pumpWidget(widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden( - '$relativeGoldenPath/arrow_bidirectional_vertical.png', - ), - ); - }, skip: kIsWeb); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart deleted file mode 100644 index a8b5264ff9c..00000000000 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/flex/flex_test.dart +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'dart:convert'; - -import 'package:devtools_app/src/screens/inspector/layout_explorer/flex/flex.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree.dart'; -import 'package:devtools_app/src/shared/diagnostics/diagnostics_node.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../../../test_infra/matchers/matchers.dart'; - -// TODO(albertusangga): Re-enable tests in this files -// https://github.com/flutter/devtools/issues/1403 -void main() { - const windowSize = Size(1750, 1750); - const relativeGoldenPath = - '../../../../test_infra/goldens/inspector/layout_explorer/flex'; - - Map buildDiagnosticsNodeJson(Axis axis) => jsonDecode(''' - { - "description": "${axis == Axis.horizontal ? 'Row' : 'Column'}", - "type": "_ElementDiagnosticableTreeNode", - "style": "dense", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-267513", - "valueId": "inspector-251", - "summaryTree": true, - "constraints": { - "type": "BoxConstraints", - "description": "BoxConstraints(w=300.0, h=60.0)", - "minWidth": "300.0", - "minHeight": "60.0", - "maxHeight": "60.0", - "maxWidth": "300.0" - }, - "size": { - "width": "300.0", - "height": "60.0" - }, - "isFlex": true, - "children": [ - { - "description": "Container", - "type": "_ElementDiagnosticableTreeNode", - "style": "dense", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-267524", - "valueId": "inspector-269", - "summaryTree": true, - "constraints": { - "type": "BoxConstraints", - "description": "BoxConstraints(0.0<=w<=Infinity, 0.0<=h<=56.0)", - "minWidth": "0.0", - "minHeight": "0.0", - "maxHeight": "56.0", - "maxWidth": "Infinity" - }, - "size": { - "width": "56.0", - "height": "25.0" - }, - "flexFactor": null, - "createdByLocalProject": true, - "children": [], - "widgetRuntimeType": "Container", - "stateful": false - }, - { - "description": "Expanded", - "type": "_ElementDiagnosticableTreeNode", - "style": "dense", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-267563", - "valueId": "inspector-332", - "summaryTree": true, - "constraints": { - "type": "BoxConstraints", - "description": "BoxConstraints(w=40.0, 0.0<=h<=56.0)", - "minWidth": "40.0", - "minHeight": "0.0", - "maxHeight": "56.0", - "maxWidth": "40.0" - }, - "size": { - "width": "40.0", - "height": "31.0" - }, - "flexFactor": 1, - "createdByLocalProject": true, - "children": [], - "widgetRuntimeType": "Expanded" - } - ], - "widgetRuntimeType": "${axis == Axis.horizontal ? 'Row' : 'Column'}", - "renderObject": { - "description": "RenderFlex#6cfb1 relayoutBoundary=up5", - "type": "DiagnosticableTreeNode", - "hasChildren": true, - "allowWrap": false, - "objectId": "inspector-3758", - "valueId": "inspector-118", - "summaryTree": true, - "properties": [ - { - "description": " (can use size)", - "type": "DiagnosticsProperty", - "name": "parentData", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3759", - "valueId": "inspector-120", - "summaryTree": true, - "properties": [], - "ifNull": "MISSING", - "tooltip": "can use size", - "missingIfNull": true, - "propertyType": "ParentData", - "defaultLevel": "info" - }, - { - "description": "${axis.name}", - "type": "EnumProperty", - "name": "direction", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3762", - "valueId": "inspector-126", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "Axis", - "defaultLevel": "info" - }, - { - "description": "start", - "type": "EnumProperty", - "name": "mainAxisAlignment", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3763", - "valueId": "inspector-128", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "MainAxisAlignment", - "defaultLevel": "info" - }, - { - "description": "max", - "type": "EnumProperty", - "name": "mainAxisSize", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3764", - "valueId": "inspector-130", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "MainAxisSize", - "defaultLevel": "info" - }, - { - "description": "center", - "type": "EnumProperty", - "name": "crossAxisAlignment", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3765", - "valueId": "inspector-132", - "summaryTree": true, - "properties": [], - "missingIfNull": false, - "propertyType": "CrossAxisAlignment", - "defaultLevel": "info" - }, - { - "description": "ltr", - "type": "EnumProperty", - "name": "textDirection", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3766", - "valueId": "inspector-83", - "summaryTree": true, - "properties": [], - "defaultValue": "null", - "missingIfNull": false, - "propertyType": "TextDirection", - "defaultLevel": "info" - }, - { - "description": "down", - "type": "EnumProperty", - "name": "verticalDirection", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3767", - "valueId": "inspector-135", - "summaryTree": true, - "properties": [], - "defaultValue": "null", - "missingIfNull": false, - "propertyType": "VerticalDirection", - "defaultLevel": "info" - }, - { - "description": "alphabetic", - "type": "EnumProperty", - "name": "textBaseline", - "style": "singleLine", - "allowNameWrap": true, - "objectId": "inspector-3767", - "valueId": "inspector-135", - "summaryTree": true, - "properties": [], - "defaultValue": "null", - "missingIfNull": false, - "propertyType": "TextBaseline", - "defaultLevel": "info" - } - ] - } - } - '''); - - Widget wrap(Widget widget) { - return MaterialApp(home: Scaffold(body: widget)); - } - - /// current workaround for flaky image asset testing. - /// https://github.com/flutter/flutter/issues/38997 - Future pump(WidgetTester tester, Widget w) async { - await tester.runAsync(() async { - await tester.pumpWidget(w); - for (final element in find.byType(Image).evaluate()) { - final widget = element.widget as Image; - final image = widget.image; - await precacheImage(image, element); - await tester.pumpAndSettle(); - } - }); - } - - testWidgetsWithWindowSize('Row golden test', windowSize, ( - WidgetTester tester, - ) async { - final rowWidgetJsonNode = buildDiagnosticsNodeJson(Axis.horizontal); - final diagnostic = RemoteDiagnosticsNode( - rowWidgetJsonNode, - null, - false, - null, - ); - final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - final controller = TestInspectorController()..setSelectedNode(treeNode); - final widget = wrap(FlexLayoutExplorerWidget(controller)); - await pump(tester, widget); - await tester.pumpAndSettle(); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden('$relativeGoldenPath/story_of_row_layout.png'), - ); - }, skip: true); - - testWidgetsWithWindowSize('Column golden test', windowSize, ( - WidgetTester tester, - ) async { - final columnWidgetJsonNode = buildDiagnosticsNodeJson(Axis.vertical); - final diagnostic = RemoteDiagnosticsNode( - columnWidgetJsonNode, - null, - false, - null, - ); - final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - final controller = TestInspectorController()..setSelectedNode(treeNode); - final widget = wrap(FlexLayoutExplorerWidget(controller)); - await pump(tester, widget); - await expectLater( - find.byWidget(widget), - matchesDevToolsGolden('$relativeGoldenPath/story_of_column_layout.png'), - ); - }, skip: true); -} diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/inspector_data_models_test.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/inspector_data_models_test.dart deleted file mode 100644 index a8e0cd0d86a..00000000000 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/inspector_data_models_test.dart +++ /dev/null @@ -1,442 +0,0 @@ -// Copyright 2019 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/src/screens/inspector/inspector_data_models.dart'; -import 'package:devtools_app/src/screens/inspector/layout_explorer/ui/theme.dart'; -import 'package:devtools_app/src/shared/primitives/math_utils.dart'; -import 'package:devtools_app_shared/ui.dart'; -import 'package:devtools_app_shared/utils.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'layout_explorer_test_utils.dart'; - -void main() { - setGlobal(IdeTheme, IdeTheme()); - - group('FlexLayoutProperties tests', () { - Future toFlexLayoutProperties( - Flex flex, { - required WidgetTester tester, - int subtreeDepth = 2, - double? width, - double? height, - }) async { - final wrappedWidget = SizedBox(width: width, height: height, child: flex); - final rootNodeDiagnostics = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: wrappedWidget, - tester: tester, - subtreeDepth: subtreeDepth, - ); - final flexDiagnostics = rootNodeDiagnostics.childrenNow.first; - return FlexLayoutProperties.fromDiagnostics(flexDiagnostics); - } - - testWidgets( - 'FlexLayoutProperties.fromJson creates correct value from enum', - (tester) async { - const widget = Row( - textDirection: TextDirection.ltr, - children: [SizedBox()], - ); - final flexProperties = await toFlexLayoutProperties( - widget, - tester: tester, - ); - expect(flexProperties.direction, Axis.horizontal); - expect(flexProperties.mainAxisAlignment, MainAxisAlignment.start); - expect(flexProperties.mainAxisSize, MainAxisSize.max); - expect(flexProperties.crossAxisAlignment, CrossAxisAlignment.center); - expect(flexProperties.textDirection, TextDirection.ltr); - expect(flexProperties.verticalDirection, VerticalDirection.down); - expect(flexProperties.textBaseline, null); - }, - ); - - testWidgets('startIsTopLeft should return false', (tester) async { - const columnWidget = Column( - verticalDirection: VerticalDirection.up, - children: [SizedBox()], - ); - final columnProperties = await toFlexLayoutProperties( - columnWidget, - tester: tester, - ); - expect(columnProperties.startIsTopLeft, false); - - const rowWidget = Row( - textDirection: TextDirection.rtl, - children: [SizedBox()], - ); - final rowProperties = await toFlexLayoutProperties( - rowWidget, - tester: tester, - ); - expect(rowProperties.startIsTopLeft, false); - }); - - testWidgets( - 'displayChildren is the same as children when start is top left', - (tester) async { - final widget = Column(children: [const SizedBox(), Container()]); - final properties = await toFlexLayoutProperties(widget, tester: tester); - expect(properties.startIsTopLeft, true); - expect(properties.displayChildren[0].description, 'SizedBox'); - expect(properties.displayChildren[1].description, 'Container'); - }, - ); - - testWidgets( - 'displayChildren is a reversed children when start is not top left', - (tester) async { - final widget = Column( - verticalDirection: VerticalDirection.up, - children: [const SizedBox(), Container()], - ); - final properties = await toFlexLayoutProperties(widget, tester: tester); - expect(properties.startIsTopLeft, false); - expect(properties.displayChildren[0].description, 'Container'); - expect(properties.displayChildren[1].description, 'SizedBox'); - }, - ); - - group('childrenRenderProperties tests', () { - const maxMainAxisDimension = 500.0; - - double maxSizeAvailable(Axis _) => maxMainAxisDimension; - - List childrenRenderProperties( - FlexLayoutProperties properties, - ) => properties.childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - - final childrenWidgets = [ - const SizedBox(width: 50.0), - const SizedBox(width: 75.0, height: 25.0), - ]; - - testWidgets( - 'returns correct RenderProperties with main axis not flipped when start is top left', - (tester) async { - final widget = Row(children: childrenWidgets); - final properties = await toFlexLayoutProperties( - widget, - width: maxMainAxisDimension, - tester: tester, - subtreeDepth: 3, - ); - final renderProps = properties.childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - expect(renderProps.length, 3); - expect(renderProps, [ - RenderProperties( - axis: Axis.horizontal, - size: const Size(250, 250), - realSize: const Size(50.0, 0.0), - offset: const Offset(0.0, 125.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(261.5, 500), - realSize: const Size(75.0, 25.0), - offset: const Offset(250.0, 0.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(400, 500), - realSize: const Size(375.0, 25.0), - offset: const Offset(511.5, 0.0), - isFreeSpace: true, - layoutProperties: properties, - ), - ]); - }, - ); - - testWidgets( - 'returns correct RenderProperties with main axis flipped when start is not top left', - (tester) async { - final widget = Row( - textDirection: TextDirection.rtl, - children: childrenWidgets, - ); - final properties = await toFlexLayoutProperties( - widget, - tester: tester, - width: maxMainAxisDimension, - subtreeDepth: 3, - ); - final renderProps = properties.childrenRenderProperties( - smallestRenderWidth: minRenderWidth, - largestRenderWidth: defaultMaxRenderWidth, - smallestRenderHeight: minRenderHeight, - largestRenderHeight: defaultMaxRenderHeight, - maxSizeAvailable: maxSizeAvailable, - ); - expect(renderProps.length, 3); - expect(renderProps, [ - RenderProperties( - axis: Axis.horizontal, - size: const Size(261.5, 500.0), - realSize: const Size(75.0, 25.0), - offset: const Offset(400.0, 0.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(250.0, 250.0), - realSize: const Size(50.0, 0.0), - offset: const Offset(661.5, 125.0), - layoutProperties: properties, - ), - RenderProperties( - axis: Axis.horizontal, - size: const Size(400, 500), - realSize: const Size(375.0, 25.0), - offset: const Offset(0.0, 0.0), - isFreeSpace: true, - layoutProperties: properties, - ), - ]); - }, - ); - - testWidgets( - 'when the start is not top left, render properties should be equals to its mirrored version', - (tester) async { - Row buildWidget({ - required bool flipMainAxis, - required MainAxisAlignment mainAxisAlignment, - }) => Row( - textDirection: flipMainAxis ? TextDirection.rtl : TextDirection.ltr, - mainAxisAlignment: flipMainAxis - ? mainAxisAlignment.reversed - : mainAxisAlignment, - children: flipMainAxis - ? childrenWidgets.reversed.toList() - : childrenWidgets, - ); - for (final mainAxisAlignment in MainAxisAlignment.values) { - final originalWidgetRenderProperties = childrenRenderProperties( - await toFlexLayoutProperties( - buildWidget( - flipMainAxis: false, - mainAxisAlignment: mainAxisAlignment, - ), - tester: tester, - ), - ); - final mirroredWidgetRenderProperties = childrenRenderProperties( - await toFlexLayoutProperties( - buildWidget( - flipMainAxis: true, - mainAxisAlignment: mainAxisAlignment, - ), - tester: tester, - ), - ); - expect( - originalWidgetRenderProperties, - mirroredWidgetRenderProperties, - ); - } - }, - ); - }); - }); - - group('LayoutProperties tests', () { - testWidgets('deserializes RemoteDiagnosticsNode correctly', (tester) async { - const constraints = BoxConstraints( - minWidth: 432.0, - maxWidth: 432.0, - minHeight: 56.0, - maxHeight: 56.0, - ); - const size = Size(432.0, 56.0); - final widget = Container( - width: size.width, - height: size.height, - constraints: constraints, - child: const Row(children: [SizedBox()]), - ); - final diagnosticsNode = await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - subtreeDepth: 2, - ); - final rowDiagnosticsNode = diagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(rowDiagnosticsNode); - - expect(layoutProperties.size, size); - expect(layoutProperties.constraints, constraints); - expect(layoutProperties.totalChildren, 1); - }); - - group('describeWidthConstraints and describeHeightConstraints', () { - testWidgets('single value', (tester) async { - const width = 25.0; - const height = 56.0; - const constraints = BoxConstraints.tightFor( - width: width, - height: height, - ); - final widget = ConstrainedBox( - constraints: constraints, - child: const SizedBox(), - ); - final constrainedBoxDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final sizedBoxDiagnosticsNode = - constrainedBoxDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(sizedBoxDiagnosticsNode); - expect(layoutProperties.describeHeightConstraints(), 'h=$height'); - expect(layoutProperties.describeWidthConstraints(), 'w=$width'); - }); - - testWidgets('range value', (tester) async { - const minWidth = 25.0, maxWidth = 50.0; - const minHeight = 75.0, maxHeight = 100.0; - const constraints = BoxConstraints( - minWidth: minWidth, - maxWidth: maxWidth, - minHeight: minHeight, - maxHeight: maxHeight, - ); - final widget = ConstrainedBox( - constraints: constraints, - child: const SizedBox(), - ); - final constrainedBoxDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final sizedBoxDiagnosticsNode = - constrainedBoxDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(sizedBoxDiagnosticsNode); - expect( - layoutProperties.describeHeightConstraints(), - '$minHeight<=h<=$maxHeight', - ); - expect( - layoutProperties.describeWidthConstraints(), - '$minWidth<=w<=$maxWidth', - ); - }); - - testWidgets('unconstrained width', (tester) async { - final widget = Row(children: [Container()]); - final rowDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final containerDiagnosticsNode = rowDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(containerDiagnosticsNode); - expect( - layoutProperties.describeWidthConstraints(), - 'width is unconstrained', - ); - }); - - testWidgets('unconstrained height', (tester) async { - final widget = Column(children: [Container()]); - final columnDiagnosticsNode = - await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final containerDiagnosticsNode = - columnDiagnosticsNode.childrenNow.first; - final layoutProperties = LayoutProperties(containerDiagnosticsNode); - expect( - layoutProperties.describeHeightConstraints(), - 'height is unconstrained', - ); - }); - }); - - testWidgets('describeWidth and describeHeight', (tester) async { - const width = 432.5, height = 56.0; - final widget = SizedBox(width: width, height: height, child: Container()); - final sizedBoxNode = await widgetToLayoutExplorerRemoteDiagnosticsNode( - widget: widget, - tester: tester, - ); - final containerNode = sizedBoxNode.childrenNow.first; - final layoutProperties = LayoutProperties(containerNode); - expect(layoutProperties.describeHeight(), 'h=$height'); - expect(layoutProperties.describeWidth(), 'w=$width'); - }); - }); - - group('computeRenderSizes', () { - test( - 'scale sizes so the largestSize maps to largestRenderSize with forceToOccupyMaxSize=false', - () { - final renderSizes = computeRenderSizes( - sizes: [100.0, 200.0, 300.0], - smallestSize: 100.0, - largestSize: 300.0, - smallestRenderSize: 200.0, - largestRenderSize: 600.0, - maxSizeAvailable: 2000, - useMaxSizeAvailable: false, - ); - expect(renderSizes, [200.0, 400.0, 600.0]); - expect(sum(renderSizes), lessThan(2000)); - }, - ); - - test( - 'scale sizes so the items fit maxSizeAvailable with forceToOccupyMaxSize=true', - () { - final renderSizes = computeRenderSizes( - sizes: [100.0, 200.0, 300.0], - smallestSize: 100.0, - largestSize: 300.0, - smallestRenderSize: 200.0, - largestRenderSize: 600.0, - maxSizeAvailable: 2000, - ); - expect(renderSizes, [200.0, 666.6666666666667, 1133.3333333333335]); - expect(sum(renderSizes) - 2000.0, lessThan(0.01)); - }, - ); - - test( - 'scale sizes when the items exceeds maxSizeAvailable with forceToOccupyMaxSize=true should not change any behavior', - () { - final renderSizes = computeRenderSizes( - sizes: [100.0, 200.0, 300.0], - smallestSize: 100.0, - largestSize: 300.0, - smallestRenderSize: 300.0, - largestRenderSize: 900.0, - maxSizeAvailable: 250.0, - ); - expect(renderSizes, [300.0, 600.0, 900.0]); - expect(sum(renderSizes), greaterThan(250.0)); - }, - ); - }); -} diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_serialization_delegate.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_serialization_delegate.dart deleted file mode 100644 index 3f1b6091ea3..00000000000 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_serialization_delegate.dart +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2020 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -/// This class is merely a duplicate of the InspectorSerializationDelegate inside the `assets/scripts/inspector_polyfill_script.dart`. -/// This delegate is used for testing Widget Diagnostics Node so that we don't have to create manual JSON each time we want to create new test cases. -/// TODO(adalberht): Ask Jacob on what's the better solution on code reuse between the Layout Explorer polyfillscripts and the code inside test package? -class LayoutExplorerSerializationDelegate - extends InspectorSerializationDelegate { - LayoutExplorerSerializationDelegate({ - String super.groupName = '', - super.subtreeDepth, - required super.service, - }) : super( - summaryTree: true, - addAdditionalPropertiesCallback: (node, delegate) { - final additionalJson = {}; - final value = node.value; - if (value is Element) { - final renderObject = value.renderObject!; - additionalJson['renderObject'] = renderObject - .toDiagnosticsNode() - .toJsonMap( - delegate.copyWith(subtreeDepth: 0, includeProperties: true), - ); - // Required for test. - // ignore: invalid_use_of_protected_member - final constraints = renderObject.constraints; - - final constraintsProperty = { - 'type': constraints.runtimeType.toString(), - 'description': constraints.toString(), - }; - if (constraints is BoxConstraints) { - constraintsProperty.addAll({ - 'minWidth': constraints.minWidth.toString(), - 'minHeight': constraints.minHeight.toString(), - 'maxWidth': constraints.maxWidth.toString(), - 'maxHeight': constraints.maxHeight.toString(), - }); - } - additionalJson['constraints'] = constraintsProperty; - - if (renderObject is RenderBox) { - additionalJson['size'] = { - 'width': renderObject.size.width.toString(), - 'height': renderObject.size.height.toString(), - }; - - final parentData = renderObject.parentData; - if (parentData is FlexParentData) { - additionalJson['flexFactor'] = parentData.flex ?? 0; - additionalJson['flexFit'] = - (parentData.fit ?? FlexFit.tight).name; - } - } - } - return additionalJson; - }, - ); -} diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_test_utils.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_test_utils.dart deleted file mode 100644 index c2b1f449af6..00000000000 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/layout_explorer_test_utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2020 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/src/shared/diagnostics/diagnostics_node.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'layout_explorer_serialization_delegate.dart'; - -Future widgetToLayoutExplorerRemoteDiagnosticsNode({ - required Widget widget, - required WidgetTester tester, - int subtreeDepth = 1, -}) async { - await tester.pumpWidget(MaterialApp(home: Scaffold(body: widget))); - final element = find.byWidget(widget).evaluate().first; - final nodeJson = element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.dense) - .toJsonMap( - LayoutExplorerSerializationDelegate( - subtreeDepth: subtreeDepth, - service: WidgetInspectorService.instance, - ), - ); - return RemoteDiagnosticsNode(nodeJson, null, false, null); -} diff --git a/packages/devtools_app/test/screens/inspector/layout_explorer/widget_theme_test.dart b/packages/devtools_app/test/screens/inspector/layout_explorer/widget_theme_test.dart deleted file mode 100644 index 95beb1f9181..00000000000 --- a/packages/devtools_app/test/screens/inspector/layout_explorer/widget_theme_test.dart +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/src/screens/inspector/layout_explorer/ui/widgets_theme.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Test WidgetTheme', () { - test('Correct asset from widget with a type', () { - const widgetName = 'AnimatedBuilder'; - final theme = WidgetTheme.fromName(widgetName); - expect(theme.iconAsset, 'icons/inspector/widget_icons/animated.png'); - }); - - test('Has default theme for custom widget', () { - const widgetName = 'CustomWidget'; - final theme = WidgetTheme.fromName(widgetName); - expect(theme.color, WidgetTheme.otherWidgetColor); - }); - }); -} diff --git a/packages/devtools_app/test/screens/inspector/utils/inspector_tree.dart b/packages/devtools_app/test/screens/inspector/utils/inspector_tree.dart deleted file mode 100644 index 88963611a34..00000000000 --- a/packages/devtools_app/test/screens/inspector/utils/inspector_tree.dart +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2021 The Flutter Authors -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_test/helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; - -/// Create an `InspectorTreeControllerFlutter` from a single `RemoteDiagnosticsNode` -InspectorTreeController inspectorTreeControllerFromNode( - RemoteDiagnosticsNode node, -) { - final controller = InspectorTreeController() - ..config = InspectorTreeConfig( - onNodeAdded: (_, _) {}, - onClientActiveChange: (_) {}, - ); - - controller.root = InspectorTreeNode() - ..appendChild(InspectorTreeNode()..diagnostic = node); - - return controller; -} - -/// Replicates the functionality of `getRootWidgetSummaryTreeWithPreviews` from -/// inspector_polyfill_script.dart -Future widgetToInspectorTreeDiagnosticsNode({ - required Widget widget, - required WidgetTester tester, -}) async { - await tester.pumpWidget(wrap(widget)); - final element = find.byWidget(widget).evaluate().first; - final nodeJson = element - .toDiagnosticsNode(style: DiagnosticsTreeStyle.dense) - .toJsonMap( - InspectorSerializationDelegate( - service: WidgetInspectorService.instance, - subtreeDepth: 1000000, - summaryTree: true, - addAdditionalPropertiesCallback: (node, delegate) { - final additionalJson = {}; - - final value = node.value; - if (value is Element) { - final renderObject = value.renderObject; - if (renderObject is RenderParagraph) { - additionalJson['textPreview'] = renderObject.text.toPlainText(); - } - } - - return additionalJson; - }, - ), - ); - - return RemoteDiagnosticsNode(nodeJson, null, false, null); -} diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart index 6db56eb0c62..82b2f1fce4f 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart @@ -5,6 +5,7 @@ import 'dart:collection'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/screens/inspector_v2/inspector_screen_body.dart'; import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart index c9f902fdb60..2e5a7faa4f5 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_integration_test.dart @@ -8,13 +8,7 @@ library; import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:devtools_app/devtools_app.dart' - hide InspectorScreenBodyState, InspectorScreenBody, InspectorRowContent; -import 'package:devtools_app/src/screens/inspector/inspector_screen_body.dart' - as legacy; -import 'package:devtools_app/src/screens/inspector_shared/inspector_controls.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_screen_body.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; +import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/ui/utils.dart'; import 'package:devtools_app/src/screens/inspector_v2/widget_properties/properties_view.dart'; import 'package:devtools_app_shared/ui.dart'; @@ -63,15 +57,11 @@ void main() { setUp(() async { await env.setupEnvironment(); - // Enable the V2 inspector: - preferences.inspector.setLegacyInspectorEnabled(false); setGlobal(BannerMessagesController, BannerMessagesController()); }); tearDown(() async { await env.tearDownEnvironment(force: true); - // Re-set changes to preferences: - preferences.inspector.setLegacyInspectorEnabled(true); }); tearDownAll(() { @@ -369,65 +359,6 @@ void main() { ); }); - testWidgetsWithWindowSize('can revert to legacy inspector', windowSize, ( - WidgetTester tester, - ) async { - await _loadInspectorUI(tester); - - // Select the CustomCenter widget (row index #4) - await tester.tap(find.richText('CustomCenter')); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Disable Inspector V2: - await toggleLegacyInspector(tester); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the legacy inspector is visible: - await expectLater( - find.byType(legacy.InspectorScreenBody), - matchesDevToolsGolden( - '../../test_infra/goldens/integration_inspector_v2_revert_to_legacy.png', - ), - ); - }); - - // Test to verify https://github.com/flutter/devtools/issues/8487 is fixed. - testWidgetsWithWindowSize( - 'revert to legacy inspector, hot-restart, and back to new inspector', - windowSize, - (WidgetTester tester) async { - await _loadInspectorUI(tester); - - // Disable Inspector V2. - await toggleLegacyInspector(tester); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the legacy inspector is visible. - expect(find.richTextContaining('Widget Details Tree'), findsOneWidget); - - // Trigger a hot restart. - await env.flutter!.hotRestart(); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Enable Inspector V2. - await toggleLegacyInspector(tester); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Verify the legacy inspector is not visible. - expect(find.richTextContaining('Widget Details Tree'), findsNothing); - - // Wait for the widget tree to load. - final centerWidgetFinder = find.richText('Center'); - final centerWidgetFinderWithRetries = await retryUntilFound( - centerWidgetFinder, - tester: tester, - retries: 10, - ); - expect(centerWidgetFinderWithRetries, findsOneWidget); - }, - skip: true, // https://github.com/flutter/devtools/issues/8490 - ); - testWidgetsWithWindowSize( 'tree nodes contain only essential information', windowSize, @@ -688,33 +619,6 @@ Finder findExpandCollapseButtonForRow({ return expandCollapseButtonFinder; } -Future toggleLegacyInspector(WidgetTester tester) async { - // Open settings dialog. - final inspectorSettingsDialogButton = find.descendant( - of: find.byType(InspectorServiceExtensionButtonGroup), - matching: find.byType(SettingsOutlinedButton), - ); - await tester.tap(inspectorSettingsDialogButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Toggle the "legacy Inspector" checkbox. - final settingsRow = find.ancestor( - of: find.richTextContaining('Use legacy inspector'), - matching: find.byType(Row), - ); - final inspectorCheckbox = find.descendant( - of: settingsRow, - matching: find.byType(NotifierCheckbox), - ); - await tester.tap(inspectorCheckbox); - await tester.pumpAndSettle(inspectorChangeSettleTime); - - // Close the settings dialog. - final closeButton = find.byType(DialogCloseButton); - await tester.tap(closeButton); - await tester.pumpAndSettle(inspectorChangeSettleTime); -} - void verifyPropertyIsVisible({ required String name, required String value, diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart index 813cc1eb0b9..af99662edda 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart @@ -7,19 +7,11 @@ import 'dart:convert'; -import 'package:devtools_app/devtools_app.dart' - hide - InspectorController, - InspectorTreeController, - InspectorScreenBody, - ErrorNavigator, - InspectorTreeNode; +import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/inspector_shared/inspector_settings_dialog.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_screen_body.dart'; import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/flex/flex.dart'; import 'package:devtools_app/src/screens/inspector_v2/widget_details.dart'; import 'package:devtools_app/src/service/service_extensions.dart' as extensions; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app/src/shared/ui/tab.dart'; import 'package:devtools_app_shared/ui.dart'; @@ -311,7 +303,7 @@ void main() { 'should render StoryOfYourFlexWidget', windowSize, (WidgetTester tester) async { - final controller = TestInspectorV2Controller() + final controller = TestInspectorController() ..setSelectedNode(treeNode) ..setSelectedDiagnostic(diagnostic); await tester.pumpWidget( @@ -332,7 +324,7 @@ void main() { 'should listen to controller selection event', windowSize, (WidgetTester tester) async { - final controller = TestInspectorV2Controller(); + final controller = TestInspectorController(); await tester.pumpWidget( MaterialApp( home: Scaffold(body: WidgetDetails(controller: controller)), diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart index 5a7c8966850..a8af4293244 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart @@ -2,16 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. -import 'package:devtools_app/devtools_app.dart' - hide - InspectorController, - InspectorTreeController, - InspectorTree, - InspectorTreeConfig, - InspectorTreeNode; -import 'package:devtools_app/src/screens/inspector_v2/inspector_controller.dart'; +import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; diff --git a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart index 5f4ea4a2fbf..9c64239ce48 100644 --- a/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/layout_explorer/flex/flex_test.dart @@ -260,7 +260,7 @@ void main() { null, ); final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - final controller = TestInspectorV2Controller()..setSelectedNode(treeNode); + final controller = TestInspectorController()..setSelectedNode(treeNode); final widget = wrap(FlexLayoutExplorerWidget(controller)); await pump(tester, widget); await tester.pumpAndSettle(); @@ -281,7 +281,7 @@ void main() { null, ); final treeNode = InspectorTreeNode()..diagnostic = diagnostic; - final controller = TestInspectorV2Controller()..setSelectedNode(treeNode); + final controller = TestInspectorController()..setSelectedNode(treeNode); final widget = wrap(FlexLayoutExplorerWidget(controller)); await pump(tester, widget); await expectLater( diff --git a/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart b/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart index d2aac2767d3..88963611a34 100644 --- a/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart +++ b/packages/devtools_app/test/screens/inspector_v2/utils/inspector_tree.dart @@ -2,10 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. -import 'package:devtools_app/devtools_app.dart' - hide InspectorTreeController, InspectorTreeConfig, InspectorTreeNode; -import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart'; +import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_test/helpers.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart index fee470e121e..a603d1daea9 100644 --- a/packages/devtools_test/lib/src/mocks/mocks.dart +++ b/packages/devtools_test/lib/src/mocks/mocks.dart @@ -7,12 +7,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:devtools_app/devtools_app.dart'; -// ignore: implementation_imports, required to separate V2 inspector imports. -import 'package:devtools_app/src/screens/inspector_v2/inspector_controller.dart' - as inspector_v2; -// ignore: implementation_imports, required to separate V2 inspector imports. -import 'package:devtools_app/src/shared/console/eval/inspector_tree_v2.dart' - as inspector_v2; import 'package:devtools_app_shared/service.dart'; import 'package:devtools_shared/devtools_shared.dart'; import 'package:flutter/foundation.dart'; @@ -86,30 +80,27 @@ class TestInspectorController extends Fake implements InspectorController { InspectorService get inspectorService => service; } -class TestInspectorV2Controller extends Fake - implements inspector_v2.InspectorController { +class TestInspectorController extends Fake implements InspectorController { InspectorService service = FakeInspectorService(); @override - ValueListenable get selectedNode => - _selectedNode; - final _selectedNode = ValueNotifier(null); + ValueListenable get selectedNode => _selectedNode; + final _selectedNode = ValueNotifier(null); @override RemoteDiagnosticsNode? get selectedDiagnostic => _selectedDiagnostic; RemoteDiagnosticsNode? _selectedDiagnostic; @override - ValueListenable - get selectedNodeProperties => - ValueNotifier(( + ValueListenable get selectedNodeProperties => + ValueNotifier(( widgetProperties: [], renderProperties: [], layoutProperties: null, )); @override - void setSelectedNode(inspector_v2.InspectorTreeNode? newSelection) { + void setSelectedNode(InspectorTreeNode? newSelection) { _selectedNode.value = newSelection; } From ebfb9c35e26f583a6da45f365476ca3b41372720 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 16 Apr 2026 13:18:04 -0700 Subject: [PATCH 02/10] Fix duplicated TestInspectorController --- packages/devtools_test/lib/src/mocks/mocks.dart | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart index a603d1daea9..d4fd88878fb 100644 --- a/packages/devtools_test/lib/src/mocks/mocks.dart +++ b/packages/devtools_test/lib/src/mocks/mocks.dart @@ -64,22 +64,6 @@ class FakeInspectorService extends Fake implements InspectorService { bool get hoverEvalModeEnabledByDefault => true; } -class TestInspectorController extends Fake implements InspectorController { - InspectorService service = FakeInspectorService(); - - @override - ValueListenable get selectedNode => _selectedNode; - final _selectedNode = ValueNotifier(null); - - @override - void setSelectedNode(InspectorTreeNode? newSelection) { - _selectedNode.value = newSelection; - } - - @override - InspectorService get inspectorService => service; -} - class TestInspectorController extends Fake implements InspectorController { InspectorService service = FakeInspectorService(); From 138c990308d94bc09ca98fa297f3cbc038d17f71 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:01:00 -0700 Subject: [PATCH 03/10] Fix static analysis errors --- packages/devtools_app/lib/devtools_app.dart | 4 ++-- .../screens/inspector_v2/inspector_error_navigator_test.dart | 2 +- .../test/screens/inspector_v2/inspector_screen_test.dart | 2 +- .../test/screens/inspector_v2/inspector_tree_test.dart | 2 +- .../test/shared/managers/error_badge_manager_test.dart | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index c219f4721c9..58e62b503a3 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -24,10 +24,10 @@ export 'src/screens/deep_link_validation/deep_links_controller.dart'; export 'src/screens/deep_link_validation/deep_links_screen.dart'; export 'src/screens/dtd/dtd_tools_controller.dart'; export 'src/screens/dtd/dtd_tools_screen.dart'; -export 'src/screens/inspector_shared/inspector_screen.dart'; -export 'src/screens/inspector_shared/inspector_screen_controller.dart'; export 'src/screens/inspector_v2/inspector_controller.dart'; +export 'src/screens/inspector_v2/inspector_screen.dart'; export 'src/screens/inspector_v2/inspector_screen_body.dart'; +export 'src/screens/inspector_v2/inspector_screen_controller.dart'; export 'src/screens/inspector_v2/inspector_tree_controller.dart'; export 'src/screens/logging/log_details_controller.dart'; export 'src/screens/logging/logging_controller.dart'; diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart index 82b2f1fce4f..0d339af4902 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_error_navigator_test.dart @@ -5,7 +5,7 @@ import 'dart:collection'; import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_screen_body.dart'; + import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart index af99662edda..2886318376f 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_screen_test.dart @@ -8,7 +8,7 @@ import 'dart:convert'; import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/screens/inspector_shared/inspector_settings_dialog.dart'; +import 'package:devtools_app/src/screens/inspector_v2/inspector_settings_dialog.dart'; import 'package:devtools_app/src/screens/inspector_v2/layout_explorer/flex/flex.dart'; import 'package:devtools_app/src/screens/inspector_v2/widget_details.dart'; import 'package:devtools_app/src/service/service_extensions.dart' as extensions; diff --git a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart index a8af4293244..feda195f583 100644 --- a/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart +++ b/packages/devtools_app/test/screens/inspector_v2/inspector_tree_test.dart @@ -3,7 +3,7 @@ // found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_app/src/screens/inspector_v2/inspector_tree_controller.dart'; + import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_test/devtools_test.dart'; diff --git a/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart b/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart index 69b74f5b25a..13ea5195c21 100644 --- a/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart +++ b/packages/devtools_app/test/shared/managers/error_badge_manager_test.dart @@ -4,7 +4,7 @@ import 'package:devtools_app/src/screens/app_size/app_size_screen.dart'; import 'package:devtools_app/src/screens/debugger/debugger_screen.dart'; -import 'package:devtools_app/src/screens/inspector_shared/inspector_screen.dart'; +import 'package:devtools_app/src/screens/inspector_v2/inspector_screen.dart'; import 'package:devtools_app/src/screens/logging/logging_screen.dart'; import 'package:devtools_app/src/screens/memory/framework/memory_screen.dart'; import 'package:devtools_app/src/screens/network/network_screen.dart'; From fd1040f3ec7ff11706ff91095e5b88537bab1060 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:18:58 -0700 Subject: [PATCH 04/10] Move inspector_shared files into inspector_v2 --- packages/devtools_app/lib/src/app.dart | 4 ++-- .../src/screens/inspector_v2/inspector_controller.dart | 2 +- .../inspector_controls.dart | 8 ++++---- .../inspector_screen.dart | 0 .../src/screens/inspector_v2/inspector_screen_body.dart | 4 ++-- .../inspector_screen_controller.dart | 0 .../inspector_settings_dialog.dart | 0 .../lib/src/shared/managers/error_badge_manager.dart | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) rename packages/devtools_app/lib/src/screens/{inspector_shared => inspector_v2}/inspector_controls.dart (95%) rename packages/devtools_app/lib/src/screens/{inspector_shared => inspector_v2}/inspector_screen.dart (100%) rename packages/devtools_app/lib/src/screens/{inspector_shared => inspector_v2}/inspector_screen_controller.dart (100%) rename packages/devtools_app/lib/src/screens/{inspector_shared => inspector_v2}/inspector_settings_dialog.dart (100%) diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 43c72cd6b7c..fdbd7804893 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -31,8 +31,8 @@ import 'screens/deep_link_validation/deep_links_controller.dart'; import 'screens/deep_link_validation/deep_links_screen.dart'; import 'screens/dtd/dtd_tools_controller.dart'; import 'screens/dtd/dtd_tools_screen.dart'; -import 'screens/inspector_shared/inspector_screen.dart'; -import 'screens/inspector_shared/inspector_screen_controller.dart'; +import 'screens/inspector_v2/inspector_screen.dart'; +import 'screens/inspector_v2/inspector_screen_controller.dart'; import 'screens/logging/logging_controller.dart'; import 'screens/logging/logging_screen.dart'; import 'screens/memory/framework/memory_controller.dart'; 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 32de022bcdf..f8694bc92e6 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 @@ -36,8 +36,8 @@ import '../../shared/managers/notifications.dart'; import '../../shared/primitives/query_parameters.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/utils/utils.dart'; -import '../inspector_shared/inspector_screen.dart'; import 'inspector_data_models.dart'; +import 'inspector_screen.dart'; import 'inspector_tree_controller.dart'; final _log = Logger('inspector_controller'); diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart similarity index 95% rename from packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart rename to packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart index d1f810a2605..2f1e53b922f 100644 --- a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_controls.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_controls.dart @@ -13,14 +13,14 @@ import '../../shared/analytics/constants.dart' as gac; import '../../shared/feature_flags.dart'; import '../../shared/globals.dart'; import '../../shared/ui/common_widgets.dart'; -import '../inspector_shared/inspector_settings_dialog.dart'; -import '../inspector_v2/inspector_controller.dart' as v2; +import 'inspector_controller.dart'; +import 'inspector_settings_dialog.dart'; /// Control buttons for the inspector panel. class InspectorControls extends StatelessWidget { const InspectorControls({super.key, this.controller}); - final v2.InspectorController? controller; + final InspectorController? controller; static const minScreenWidthForTextBeforeTruncating = 800.0; static const minScreenWidthForText = 550.0; @@ -113,7 +113,7 @@ class InspectorServiceExtensionButtonGroup extends StatelessWidget { class ShowImplementationWidgetsButton extends StatelessWidget { const ShowImplementationWidgetsButton({super.key, required this.controller}); - final v2.InspectorController controller; + final InspectorController controller; @override Widget build(BuildContext context) { diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart similarity index 100% rename from packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen.dart rename to packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen.dart diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart index bcb6070927b..b43e9ff50a9 100644 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_body.dart @@ -19,9 +19,9 @@ import '../../shared/primitives/blocking_action_mixin.dart'; import '../../shared/ui/common_widgets.dart'; import '../../shared/ui/search.dart'; import '../../shared/utils/utils.dart'; -import '../inspector_shared/inspector_controls.dart'; -import '../inspector_shared/inspector_screen.dart'; import 'inspector_controller.dart'; +import 'inspector_controls.dart'; +import 'inspector_screen.dart'; import 'inspector_tree_controller.dart'; import 'widget_details.dart'; diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_controller.dart similarity index 100% rename from packages/devtools_app/lib/src/screens/inspector_shared/inspector_screen_controller.dart rename to packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_controller.dart diff --git a/packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_settings_dialog.dart similarity index 100% rename from packages/devtools_app/lib/src/screens/inspector_shared/inspector_settings_dialog.dart rename to packages/devtools_app/lib/src/screens/inspector_v2/inspector_settings_dialog.dart diff --git a/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart b/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart index d3d3f7dc8bc..aadfe082177 100644 --- a/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart +++ b/packages/devtools_app/lib/src/shared/managers/error_badge_manager.dart @@ -11,7 +11,7 @@ import 'package:devtools_app_shared/utils.dart'; import 'package:flutter/foundation.dart'; import 'package:vm_service/vm_service.dart'; -import '../../screens/inspector_shared/inspector_screen.dart'; +import '../../screens/inspector_v2/inspector_screen.dart'; import '../../screens/logging/logging_screen.dart'; import '../../screens/network/network_screen.dart'; import '../../screens/performance/performance_screen.dart'; From a9dd9ca5d2b9234fe6f3dfb033d0a78866a7e9f7 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 20 Apr 2026 09:51:18 -0700 Subject: [PATCH 05/10] Update goldens --- ...or_v2_implementation_widgets_collapsed.png | Bin 37393 -> 38621 bytes .../memory/load_offline_data_profile_tab.png | Bin 31964 -> 28761 bytes 2 files changed, 0 insertions(+), 0 deletions(-) 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 e148d4679f1c1201ec44bbc345afa75fe3af3cc5..d719b72f00a5a598fcad2ef8a32f7c511baf70bf 100644 GIT binary patch literal 38621 zcmd43cUaR|+b$Y)ETbYK2uK}Kid5-6Q9)2ZdXp9h5Kx*3p+j&Sq=Sm|CL+BJ(z^u+ zNQp=%KqLr}8bS*#WUml)=Kc0}zW3exoIlRGE-_{OR(;C--0N8h)74g|JI;0-0)f!o zyrHTOfgBfrKz{k_*b(rFqmXVt`0p3@Yc~y#f#QG6HWa)+;I6NJ4N}s>F$;nG2Dzzv z)zCY2Vcaiu91+03N2;|GP12O8J=K0a@7t?d<1thkN-*T{mlM9xLB_NG&EtaVe|lvW`&ei&=)m-Gi6S z1>-bqpHH`6)a)Hej$9g4t*|xd?JMucxVO> zRaLdO$6Hh%JTZDgtbsK*BXHEB`hb*tX(zgz`HZd`NJ zvuC{y&d%i9tZqhYC01t{80vQdnN7#WSr{2(dq+kXnga*oP~(Fy4zzZ3bg*)8L@q5Y zIXF5N5+)$Gvh?#?zI?g!x^MQ|(ozX#aRCcs^4s7J&GdJSoC!zNb3&|KZkkKfmDie9 z`rj0`8iNikLLjMcz+%_(zC6}_^YlSBl`9N+PloOW%}7E6e|r)Vl9g+JZ`}B1!2Kaf zH+-5GN)cOoK5!PnVk~WkA_bl{uL`_S;)5O_el$s6e=`qf*5%;jj0y|u_%=HmW-B4? zH-2cfZFAEbtVYYnIH3zkT0Ntlozo8+YU*KDtQsw*=an5XhhKVjVj|qOKWhn_S2>lK zAArbB%k3R;*SW%w9Q)fZQcr?-bm_pNb>~}%qQC?zDk@af)lo$*JC7Ez;g@>Kb!a$hd83 zyxu0+=!>JiXoAgs_pN<*P_J2Eq?br7z0UdDDzhc+}1y@re;T=H-SK+(L`Blhj(Oz z%?BAl5toDh+zVAk2Vr3y8R zF?T0;ZCgK(LcVui_pqI^bS>DcJsvbmr(=iXN7J=k$5*QI#U{;rLLkf)W--m2`TN4A zDGr4)GcYVJMo_)=xUGKmm?c5OlKmN$pS_;1qloI1=zB4D}4$H%2bg!-^pZur(mlfr`W z8sehSVWy);k7nr#7>=tbq(xhonW_Zvg|}`yBkhz2jIk3FR(;!(aT>fE3nK;yvX-ye zG17M*JMI zEiROmL*6ZK^+x5AioLnQn_1hlaYse|+&Q17rCMeY&=`;xjm;h**z?&$d%+?T%xwr0 z@4hWs8ucw6g6K9TaUd1-%SxoH(&-vX`J{h{D=`$2ib^iUe8twugXHCA?-hqL0RI3WCSAPXrrHN|3yqerUbjBmN zSclman@Hq>Dq>Z&m8apAvf^UtiKaZ}`iedw0Y zIH%VxVs{+tobPL1J+Zw*{2HrcY>DjPb&Ylnm`57v$(L`t_sH{mE{+<3&28%+?Q7mP zX+A4E?qM^qJ~ed|XGz#zFPP6du=6RB^no8r%q6zTHf7Fj6OfdnZu7<5dFC+5a|UQ^ zHn5MZTwKxFnhC~@WR)jRp4e3AlG%IBv51}*=NpM(`u@0yDT-3yN?+N%QrO@*A+xEi zM~p{#V%W+Xq|-}zRDIgFO%SLHI;V^*o88DCHxpc!it7Tc-!N5A5T^>}ZI0|bn6K{% zgDcgS&us@kfEB^}IAfIbzRQ8&cG8EPOjGSP)21^vHdAHZZhp6vA&>{w{C>N|`uVER zjF{?%>CXajAD?OQ;Y3`0G^tLr)*@P!M{oV+m*5Axi|p9V3~BQC_7)m@ma^D9ZiEPs z`Hn}GBMDhtvP1y zcYlASy=8hB+w?-P2t)Z<33u0V3Wugk#BpNx+E^A7<|0HRw%Y)hHSPS~J0 zjmFvVTF-vwCBpYQ7keir0^dV=u6bKp9cxyZ;P1EZx;7kQ=Wl6xjHFUo>b$tPC@v$z z+-fcUd3NpB`Ivqz?DU5VSwzj!`sEtw=9;BlZ?vnW@-!l&d0vwk6QiAGej7=2oC<;Y z3U4fpv&>qT>AJhCE0EXvaFwNv-)e>qtil!2N@fj?s|zxpAb$2%RYgeVrj^%`YYtq8 z%-Mgta>=-V`4Tq-Qs5*-^k{6PS0xRjiYyuo|l8&x* zdHKe|*p|1_YFGKQZo8=%$|lw?_1&ccV3om}i%sjrl(W+hw`vd_wh~;(U4CNSP-E34 zyGMr;(EyJz9k1kmJSKVhv~zJ^X=vU3fsbHF@YI#M9q!E;w?p9!48_GFybjT4Jq}|8aE{A}% zWI7(qxF=#EngD!?YPQ6{$p}b4Pl__Du9NcB;bqf$FOs^eD>+&ueXw4^3c#?AYv;vY z9!u5bi!JtHZhgFNfDGpd;)}h3`l=dOQV)TcKGmMt#Eg3(FRg4{+ucD@dQIy*Zu)O^ z!o$RnTS8?Xv(GnUVOv>pixm^YZ|=?)kxpurf=z2_QT5-QLNe<>w~#=RcusN1ou+KT zD0h{BkJl46qx*$z_LI%Z4we#V@vtLhN)x`V0cNMB+Va&Fy0?p_G<&b6AbF_@zBwb? zeAbV2>X^#gn}P|Q06m=Jrfg=kQRQM{H60u7j#lQEaouxj*cmqvJ8%qgB>;GkcD!Ro zWX{BaR)C(%{JfB_PaKvMILII0!J%btUp`WR;z)%KuAX@7{}e#_B#rB3rm4i0$6(K$ zb5K|y7D}66RERp1S@W#ILZOu{{}-JwdIJvxqBUh=t<#M{CZTpFycXROk`LR#fgsT% zH!pxs>020elo&JQmz7@R@I_UmOvDP5oj^-J5<9-f|Ubg~xQvJ>70 zN~Ez>u!x_(&Cf@0lfS7yL*s4k+;QcQyUY~>-8Nm@*iblfP|`5p@DzG2U?$1FjX<7g zjwm*-ZncN))KV4CdU3Q8*c#@}c#)KO<41hYZ(KZ8I%uEc7Bqsm+-I<3D^MzZ zAt{H5u9U@M>Wu6&0zAh8cXDme>5pnwd%6#XUr|*Rwycu^{>{4g)pcS@^A0BtnkLsJ6_SQv+m}c^X0CR-f-j?H( zWJc`;oIR>eQGq#Sn#s<=!$WHCtInQ{)A%+Fhu&M5B7Rb#2XHnFwiQc}1#_HwbhJvn z)&g`UKU93sUHI7zYRrKHs$#v1rI3zJbxEQ_>HWV9womin!i6Tz0qyQaL_kioy*q1B z0B2Z#Ix$da?FB~`&B|4a1~k^xi)qhWa?Wb;u?Jr}Z)Rp@SCOfM+?J$no#ZrPXM--? zHswmW0Mm+e#zMQT#Iw8?_m2zg)hlvEIBie6nyp^%^FP#Yl+7v)?K+Vj}ap`_b(ed%|!{zom;K?ZBIAJNYx@mnyZfBu9x>_rrH9c$JHu^IEs2bigPG2*m{_x1BL zXufX_i#F8F!GiSJ>pMK>MS&C?9bRp7~4kGh$= z_dvT9Rkl30FI|_{8y99^+HJBl#RW*}iN3DR;(!6tOrgUlW&)Z%Q$lO=pXe@na zvx)SSWioXb5u`>!UCoz2PVn&H)M49tV{RxoYTk-8>OF5RmN6auXzJ$0GrgZbKU{uX z8)o}jKmS8{7MIdUod6HuCk%znDg-3XmG`S$*j*887N1ytGK300E5yUYKvlr)UKn+k z)J;rGJiNTnh={*_18m1|?J6-bH8t{XXZ&>#ibRO*WdnomNcG5Nz@dI45Mr9Q3(;-B z?wGibi{>UKSovORYf~#M5o3-(EaDC0Z*VAK|8mfiY1*hPMGY34U%<7mcEt%WYr=N? zm5n+OSo2+9o+j9aK>2=1?4OjEZFXhmJ?!1 zidpPOXA-Q-ST(Gq%6b9z&LI$+jpAgoNgT# zI0)(Q(28lku3Ax9+19)z+svLKOBvd1;fMNo8&`QKoTV&eko#&jMzJ!7n51+ik|IgX zlh^PMNC`}vuJ_`GIo1{=2FiOl+J%Dwv~#CoX2(r5?{ zoZb8=I-jb{KKL}2`JvL71glYQhPeC!Yw~u))Icc|JumfEm`XlrdkreN41Am>HWF@W z`;6ap^>Me@h>!JaOifG-1>uvb0zfffVTi;-d%C~mNJ&YpmFuJrmH>}c_hiQ!>C#(7 zI{bE_{bFbDh<@0{#zw8nVaT1U3L{gl5iDZSDK2&5`wsNkv&WKu0Ix)4AtsYBL2Wkx zGvGAxhH>zqm#Ylj*E226RV7Be_Fm}i)C{5nLE3eu*yDN_lSfdIYWJ_=#aZSC4p z?Vf_7;)Nuoz^B}%!&KF>8skclMS70X&x0WBTruwG+uk-5>OxA|h~O!}W`*|jL=O`i zxm#MBJ$j2a*2mTN&c};do)L-{i||lwp~kwTr6vHRh$x;1FETST0aKAm{58f<@N8j% z7wXHfZX|Os(}83?9Y<)qn7Q(hGXx^+0KyKDI1K#NFLVbr@HvJf*?Gxi}!{TYh9r42sg=>bE-?0tLW0 z>?0bgAOpb1g$oy;1HW=mUuI=x83hRLs&11OqWDQKY~Eb_h3=DQd zmMH7M#TD}@6`(pEo>%4N8`&K}bB)XI0Y5aG+l^qq61!S=^3yIr^z4?FmS9|_A$;tq9foe9WBLoQiwd?7`jJCEmYk*SLHcCJQ zCoY81;$w8%IG5Whl)!B5mLn&G`Q^?`d?hokVZuom9=KBe^&mu7`3LO;7QW1jFkxDe z#|!8h^r)wpUuUVRmzT+I0@^A1(9ws|G2GnTG0nSccwo!ZYu28_p>uT~G?lpTJ$xu} z5e)fHB|^vH+G5jF)pe%A3itG>S8rCL%-ib?EbBx+*pl|VdvgXxzmoOcbT zO-IsWOp46r2uhG3`LCPoT_T!FD(-JyT$FmkoBrs&x8)HB=+x8{RUVX;mCGgEafa!E zGqp9Vt6nIKY=g)ke0Q{<&7brP8A|cNZq&>)ns7_I~zLEVIY#vXn z{AQfS?X0uRe*j0Y<$wNous`?`KmaPpGX=Xg?&=3-v2k)CBJ)sO`&k~oh%f;)L5;I$ zF-Unf2$lJ6M|-?tDo)a&ug*Wn6X`c!_Q*FMBZjQ|d40UyA?2v_J4f z^KAV~$AjNbI_UsB=On`p*bCS9Wf#U*2TD2}qi1s0OQHb`b~7W)g@$^&dBFj{ z%q`Y53`khUeFV=!@I|>*OFLf?=K;u%tFk=!8dQU~6aCgd;YQ13EEhmgZ$_NPKM(YE z;8y`erH7%xtP=Nb{~C0kH#RIo8Eqe(-UKeJ=PvLCpH2 z1ex8&(EoS?NkkkB|M})^5u+}ZqN5eNv>=`zuOXM`e_;YRmsWyq^S?ay;|-)s;UBHv z8T=JEjUT04`wGL4*PyFevv7J4x*R>l5ehaGEH#M23|8>cZa4fs`Rxg+yJBb!R^hlT=tam$Bm@$P|E z>$0DL#2ee2Q*}VZ9jsMUQ~(FdC1hL_s>LT+h!JIu5Wjr+w1|iZRfBGi%g`F^?*r85 z)Z0gg${tRC1l*pwr59l30L24`XYi(hMa#Jid^d=^0d^_4oih0s_zHL^^YT^|)S1neAf{FYmV4O2NG)_^om|ajuLTWen*lvZacd> zcYXuz0eCA(04Et3fY__4t7lbLlkOf$g(#LKr9ukV|1@sY8SJk^YcGL-N~|+}y=4oZ z0dww-LGDf^AG}>2pa>$;2o|`%IDkoZ4gKzb?wg#Pyo3$mL=O*}HcZmF7R-7MmINTE z#LvLGiA9-smD7kBf_$CDk_oo?j()C+T;8J~nI5oFTQEOV?03rvQ^Rk{ z&Rtm7G#Jn{n}ZF*xa>SGnCA}MM56pARxw6HL)(htjFl5=#U%GeWX}F(yzK&~F~XjN z2~H~Zv6b+-{8COz%472n8~FpC){^oEjOTwX_RT2DU$>w8dI|zrU+M8`l7w!Q!BVSc zK5*|j_%FGq;1@iB!cznIPoF+TCfIQS(iPyUb4NL7mgeQi#n=wun_>V`0Yu5Yi$ZZ4 zkx^0C0kEN$`uW)U;n zK`@jcHfpQ)?uA*1nye0og@plu2eLN*nVt4rR~)b@z(#{|>If5NbabW;$X1X~ZgH`* z+JWnY5438g?WfYzHD^}00N8aO2L|1J zZNA}~B34lKB(QQIGM0`JO6Yv1k^PUFpl=``&$T1L5_ENC${JS!D=-m4Tadr*a4N*a zmlz5@yK!-*4see_KK#Hv05fQYH38xbkS1a=YL4#YNmVF3uP%_U+-}esI7q6(70jbh zsQFQv<-bx479LpMzp5mG zQwm9)OTA0|`2EHrNh}YO9}*oM?L8T6&;CHO8ojzWWQVB|p{t2An71Eg&BZ}MRj^jO z&%HNB?1k*peP%33U>z=tDN~u&aFYfeU_UKU)JmY5|H_y<`Zn$y?JfA581SEZw$JY4 z)Tajg?p2&d?q>5(J>p)BP~PFk*Ks=BFNJ1ZkZD{$ODq+I1yGM4K?yQ=evax5XP&|8 zb~-lS%fgwo{}fc(+jZF8!+W-QWemfHim4SLZ!cZqWFUf(bt9;2W;ls~1>P$$5Awl- z7niJ>?K;4r?|#kP)RuBI)LeeGOA=oPb}7hb7MOq-lLp*bu-o_94Dt=BE0jyw=ZjhME`vPz|9LpT6(J6U7RC4Y?4Y{v#KF=eW>AL3xQ6-VNA* zcQiDRs3?7u$!lrsT#Bq~Nr7iy&aEn_@D$<0Xt5GGp zjFX(;OK&7x)Wyg4^7g~*thp)@-r5f-y5#RG46SV9pQx%sd|dn?wL<{^247R2@6!`t zDlEz9NHp`C`)0}k@VxXoXkRMV=7ULPApMDfXU+4dBlL`6EiZ;n2<~H!D^uOxVfZL^ z*|wZpDLD-;t25nYi21=RQNNdBEGEiQmltJr9%k3fWj?CGe?DvQgsHT4X4HGF7gexC z1ajRLFJfMpmwKB#T3H&MrpR3iG|1-u;%rVzAiqofBK`fjUx8BRxC)nz0X=*WE^oEG+)oIsv$?|a2U4!dTSFb0c@Bhlj7u%6=_-xEr z^c^0mEJFI>DhI=3vqyD8yKR95c0qD-E^3)&W}OHT#iIK9RgfCv&^41u$6u61(VlGfsZ)}<~jbI@3#Y|e28 z?X&tyYrt;i>;~42FvT>ABq`o7v8yVj z&-W~bcGtJPGM)CUf74O3G!`9uj}9QS33<=K5BYzDX6w%`4;emc&A%3YKDAxRt6_vO zCRLi)p2P_B7#RQ1OMxW%!))}HqCZA#;C_X;(kT{WS?@U@H50kvG1de$qdB{7Tc^2+ zAB1S_bA4b<-R=0w`QtURK@h}$Qt-IIo2Oc5b(JI%y&Yy+9@cUIBXPHxpdJldar>Q_ zrsa91PqHCcH(zs(n(5@t<-@Mw?>bgqNGnG?Cw6G|L`CIRN*9O#dz16PDRH^|0q)uF zE!JfKyU7{p>fLx2Jfk|-d(^bfo9lUdxRI)-Du1 zX_^P#j@llubaT^@ak1(%k)jeO2^aGrP6lv3vu@~w-Mcfkyu@C>R_|;c=cvj(y`eB9 zdrfi!Jka&T#$Hy*>IPWeGlv-`TTVf(qSM`oU@m2y3g#0umtdDMdz@;rhYcrNY!ZB+ z51FrjX*nhGD6ru!S_BDj0VwPCqhJ$*_}rc3i0vbcn%VlQ*=HDz+WXE^)+Jv_5BQfi zEv5;y)*^S_TU1}=^d~Jgx>?!Vm0%J-7nCY-*M5HY*W^2t=F0LyScsjj7GLbau>E#F z{bomXl!=E>M_%V7k1P(KW1^4qyzf9D~)@Ie%Uq8qk z$kYMS7|nSuUSW+gYp0hQ(_3IvoL@sJF1*hssaK$?N%wml3;hdRMU^4ldaGP<@pg4< z<%VRecy|qb>E8B-7qcR~RADIubei!HG#N6aJ~ya@FZwG_X5gSb@R z%(2zsKTti=*T?wy@xakzXodMMZ+x7Ip=E~R(}?<|y85~8P-=v_=^(#nLlR>Q zogf#_ch6H^3T%0%zr2gD?ParOcby98Zsk2kch?39mwSy0Toa$zHHAsQ5~tz{el9bUo&_qP*vG3y%%-2fGndptCi5Al1x z%BBW|KvT9I+nItDj}*e6Y}mGe54!9P4(CVH8T@K;6_Mk+kot{C3xc{zjvQvt|Jl1P8g6b;hMSOG6GA8)(=^nP_< zOis`d8;+og)Z3*)+288wx9JmU4_u~5 z3m@K$?!(~TiVU@chKQk-lxuf##U*Tq_nmhJ`Dy8N6}B0pPG z^g}R}>f-t6(9qm;U%@I@~FvuGbe1e?n`q6Tbysh%xpNsH6@;LtjtTl%e?%;dU%6wL53hv7~k1{z9l_<}> znFL~pw7%$ha%L)IN8%ZwMz|`#{kNaf6OyEDJ+;GC%kBfMdwY8|z*~b%uD^c&pm(5k zXXl|?=Y_|;2}0xsfoD;RB!YB#S!hk56&ZLl*4RbK?PJE4S7*>iNytILShI{l-9yw-X&z{r zTC0+K&?!xvJTbxaIClDyqc1b>RBS>jJG+jfqoMha%8)CZQXa|g3&j-p1J`%Ue}p}O zHDWAV*Pw%?OW_fIZ`@Xsd%k_-V;?#5^D*6{ba3@rp_E9>AOZwP20F@TG1V|u*@m4b z1#K=E<7u{D_1eas^{Bs2Z~RpLAcL@d3_@$=|C&Tq!+v?t#8C|sKcjeLmG~hbsol$h z8PgrIv-`*0pFa2duo7L_n05WXM{Ulx{#7qQ0*(Ai z>{fy6>Pjc}OzCZ}z=3rgDA5*R#O{K5$q_B4m0&6e04)30)d7MENH;xs%9#5%C6FmK zm1bh4hX`1@BDz#JzeGz1>?@Z72O$M@P3H(908^Q9|E*d4RL}HpJSvi!rB5rX`oLGg zz3qgz-LoCIxug4=GPKQ8IJl#mEG{)|cA1V<# zL@MN`yZqj;!zsF71+?P@>HekGlV6yIL^@=s!PWQjiQ{ur%`?P!lJ`q7lU3F){UB34 zlx4$+qNJCfc=zm&YRR|3IDsc*Nb!ogUiNVtKZ~5i1KnTIO?5))w^+=Q(fQq^)AGT{Z>-1fF|rKa;G+cfb5A33?R4HmEF|*RLrRJ$g3ZF zWHE*q8h0OW`{N-<;8b%ocQmR58EwGi8XGwS`Z84GVr;&T9y>#s+tiaAxtyB&^H0|K z5N+{4X|JRt8@2!P3kfNqDXvX-OE7sUonlB~>ggAPM&XI|eTp%L2lUDl3-KA8e=ACD zJIhL0Aj0HtE@PxU{>uvlmb>xylD|y?^IuO&$)DqHwK>n%UEsmx=cb1*Vz?vOCjwNA zY+M_N+jSco$p}2kaWD-&gmI85b}lB?Q?_S*I8rdWpu=q*!PH^ym;cLSl{$~&L!SR5 zI|IxV<5qO1HAeO*4wvpU){rAv_eLW2N57EE*Z~gR1(0jtM`)}r7^P#E;W(??&@9GM ztt7!3Q2QAjab|?-Re^J@S)DE0?B7oJ#>@>~s+I+Z1X}=mT1h5>binl@9dK!*h{}d& zw+4Ls2N^@_>56+kFyQTNQS3H=Lo%?c22fzQ4l%Enp!^*od!_(t!eaf?l`;3(@JlM; zKM6*Xck5Ypo6h)RBg3Aez{OD$R8Lxri6tyn9Poc*9({xs=W6oRTgp_G;m}b?3)d8D zd|faKB3%ZCj;~=RTMkb~H%IpNHA*V$+)*Y!KfkG05u(W;TX6~G84yBNrfX)Oy>lS< zDEDqJ59N%zEJc~Li4>clKCt`RgiWV|M}rQ(tSpueB*C#tL(ZvhK;Fz$N&QmlJneKu zTVC>%z##4fI`TTQglTc=!?RIR-#y3JsSf~~w6bKJi%1#tka_i;bq3?S6EAHW{{9E0 zLR%t8vWkkG?wGjT$Ww9fNtY{(6OFRX*GG`&sbP0`?|9~@HszCWFQw0=~XstExR-&eYiF5Y-l+Q4Eeit$p zR_xOiAa)XkPaif}Pjnm-jtF&VPegk9nplwEmK2~;C(@&I8Ya(5$862ITInKa!+)zK zWgCV^Xx`6=`<>6``_RDx&0L;3$Ml~g0>ymZ^ccGT2Sh+a_0+bzGLsL5C%^-J7Dg)z zbHx95W3qGeF8@zs86@BT7tQ@lCdmXhd`>*!@n0j|n&t$Lk z2^a}V59q{ylmv0U;E)vX%CYApZ%@_eXG>&Jr3I9AK3dfqyl12b#58#ArE++UFn=7Z z9L(rSye=KB$$LhUjpmMYUH>gd{NGw3RM3&ZAP(2taK|g9J}5#K;yHnGpAvxX=h1sZs)%uhUyf*sW>;y`2phclL=GnTKk{FO-!)k&@F0ZNk12{ zXK`s16>_1HklQ!NhYHC8tb!O+l3BKR)=Aax=L2trX)*Wee}MvBFKcWb(0Eyh=_W{9 zv+$keH5!QToDL1W!Kbq;KW>*7m9fYT*`+LuBcHxlPvc)k@6Nfs-8zs}vdbp*-1;#8 zk@A;njEoy&f4(oOue7^aq^UI3iXdu1mL_vB>t{)IW4x{-iiA*+)X%JQ(}CzwfstaEQBJ@aj$-3>!Fsmh2Pg zN+D2XYyG9*;nrtm)<%QWx%I!Ql6NKsi$N1TBkQQA|1ruN4x==A^=2`uZFMI!Xwjw0|;%C)p5=^bG@2OYqoo=ey@N_4> z5;a5qAfCL8P?U^v!@k*w2>U6ikk@%%|G2a&T@CX`bj%l2+STD{^qa0`!QjE&7M#4>Or{^Ma0q*5 zn`q{Lgbc zGJCYm0h*(x;evu>)IcPyed?ozR|W`KV@dZS#YmplRYBU$j`hw)Mk_g^3D!I6j5#xL zVOuFhPzg@glPS~xLH=iNh9Qeo9+Xys-WdEN;p3zBRfZqmKm-&IU;p{$xlM9%ckfRw9Ayz$VNS6gKdKp{mon8wkSaO6q zmNnY7Xf_(dQgN#;@XnpfZEEU_+@+0K7bKG6TUJ3-kHHrT$eZTo zU1^HBueC;|MxM_I1)PdYFKyKtkP8VTgrN5pDPfjJW08&U^`q{%FBp448A|gWhOTU5 zyYmcs$ZSs|+0AZ)6eR5uR--@zdLf#Gi}yOv+o=f_GVs`N97*P0>h8tW{r;O zR4iclJHQbrFL2VUkxbc@sbDGpTM_c;j06Ru4etUv{Cm-5FVNTJI45NMUq4$&OUVDY zkIDCIzLb$Nnfk5KQiSM)Ecq(^c0v3>-RY1ReoDYA(~3^PAI_g0Z+SC;z!~>6IcFH(%8!$_imcAH!1&PPW0t@aRS?KM z4My!>TMbfyHx@B0m}-jf5@n4XHV9pQix)~>tec2)vm|^B=gJza=)M;r=1+WA@i(Z_ zwRbY19V=1i_af~N+MyZsjjreMzmu}Aj9mIhM%CTie`H?0uyoHqK~2H{UQSyIcvn&z zNKEeWbGi)U`e{cd&TMVOv{m7wTYnl7g@1$yeQ_4LO1^6jPUCgby$b-f_Xr8>Rlr#2 z8&jk9#)b(`*d)rx4x&}u-K|-!CsDG%9xk33>a4!n5we13a%EIRa|CcW7F6V2dR^STzSaL!h|+!4h*0IfmkRtOz;MIn`pm<5y0zL zP7S{(9m!y)YFh-qYrXa7=Qw4Ozo_TpHUbyvHC6^q_nn*lCAKdGUWOYTZuHZ^ZOs0FU}W2Q=h zcjRP533+NiWfStUtKiM`$sVUB!N0k&pi$lIKX5nx#cgH&)9cYXtE7H5;geKQ^iwz? z_f=LsSpH-8CbRg5wU++>=-!+rgL8i50#LjC4_)1ynu^MY*Ud*nyLM>S;j>94^A~<* zbeJUlI3v7qFTOoGQb(b0e3TVVS$<>M?4Vs~4W`kb+VRm4+(XeY)_rxN(X}CAf2uDP z{*PRpt|VW4KA(`uCF83Sv5j66i)0$Weqr*T`aUocyOfS_!{bIz$ATwhCsg)_p{raa z1UzZp?~IzpAB~7oI(b&m+l!7aibeQR+u;csrFq3iI-%|k zX=$8Fu&b6+b1I5V%Ww^-BmdR`gZROPW%O!Ue*UH8u99Yvn&T27-l(qF} zPDN(mcijfT%@P47L3u1k@Oy`UD5FE~HSE)akl&2J{_6=dqjxsPT^9YTULR|y&&g@S zVguF=63X4T%4r7gg-*xe*7CtpI5l5{@ET3!<e1NT1ogv~&KQ7n`M zZU&{2h*tuYWSUZ=sxn~boW=GYd>rZ?_qSK-uR3pKQGI!)b{u4ITt1OHev3Hnsq1wA zzJlw}^)K&}Q&P}EeArFhrt?W*_5A4pI+l5l;3rc(Urgv%T&h3&VGUK z5^K8oCpM7To0F4svoq{?c>m*$n$g0U2pq2O~5#^euY;{_OX&g9GepzKn=;y zEzb>sN4wJI z)hn7z8ESuc_xNZ8xYv1W`peG7BEZbmC^89~@sS7?SJW5|hYR0+^ZNC31MmvuM{8}5 z)!u#I5lD8a=hZe=Gt_;}rM1D8Qsf|KB<@xKr$u?wh;N@V=a#78=xsmWQamex=_G%A zK~F$ebXb=f%c!3CnWYzbsWhF5+Xqp)Hhj$VKxnP<+F8R*x+Dn3N`_kd0annO0M$nJ z=jDfoFnSrKF8Jh`5R4`Kt1(w;y$xhiJJOxvtB+d^{%Gu}XDf4a1+mmfU3&;*R*xD~ z-M9*dKjJB0Va_AKL)#UR0^h4{{|1{y4MhJ^{uy)Vf*vcUjuOl&(m{A~Cm_2AKG@5a z@M3wdv~o8N3WvPh19`JKocq!Ufe3;ml5Nca}2l>-70C_L0aMI{WMlANOXQ8G;$boP0W*&W|= z>5{>Qch&1HjSQ7+yrZ~*bkFa&oyl_K4$^w)-B&olg#0d5ZgV3kyqi)=@l+{l+tuJ12D(9o1^77~L9MZ7F{5rdww`KM(A=j(=CBBkau3 zdWn`2WprW)#Ig7@=xSTDS5L(*rpJU*FHfTRVIknhY})N&S&o&~-M_axo91;f9i7(E zY7^spQ;FM14t%}!tULXN5csQ`Dv%A*>~T-@TZPGv6)(l zzTZNV1nPc&ECTh@M!r^OHYQ3_KY(;8QAiXnaAo1CcOdxj12*

?2fz&uUys1`G-A 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 B##;aY literal 37393 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 literal 31964 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 YA~V*eCE3e5#+{4lDGTGglg_{Y2TXsb(*OVf From 851a5770c79f2b1dfab4e4bb58b8a5ee9bc8261d Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:18:54 -0700 Subject: [PATCH 06/10] Add release notes --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 05805dbad6f..447ccfe65ff 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -19,7 +19,8 @@ TODO: Remove this section if there are not any updates. ## Inspector updates -TODO: Remove this section if there are not any updates. +- Deleted the option to use the legacy inspector. + [#9782](https://github.com/flutter/devtools/pull/9782) ## Performance updates From 7b724e09adfa62105f81eb45bc779ccb6ecdbcfb Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:49:08 -0700 Subject: [PATCH 07/10] Attempt to fix memory screen goldens --- .../memory/framework/memory_screen_test.dart | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart b/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart index 79befb2bf5c..60542996767 100644 --- a/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart +++ b/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart @@ -63,14 +63,23 @@ void main() { } Future pumpMemoryScreen(WidgetTester tester) async { - await tester.pumpWidget( - wrapWithControllers(const MemoryScreenBody(), memory: controller), - ); + await tester.runAsync(() async { + await tester.pumpWidget( + wrapWithControllers(const MemoryScreenBody(), memory: controller), + ); - // Delay to ensure the memory profiler has collected data. - await tester.runAsync( - () async => await tester.pumpAndSettle(const Duration(seconds: 2)), - ); + // Current workaround for flaky image asset testing. + // See: https://github.com/flutter/flutter/issues/38997 + for (final element in find.byType(Image).evaluate()) { + final widget = element.widget as Image; + final image = widget.image; + await precacheImage(image, element); + await tester.pumpAndSettle(); + } + + // Delay to ensure the memory profiler has collected data. + await tester.pumpAndSettle(const Duration(seconds: 2)); + }); expect(find.byType(MemoryScreenBody), findsOneWidget); } From 41773cbe21a697bd453198f865922c5a51f57a38 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:02:44 -0700 Subject: [PATCH 08/10] Undo changes to project.pbxproj --- .../macos/Runner.xcodeproj/project.pbxproj | 86 ------------------- 1 file changed, 86 deletions(-) diff --git a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj index bd41a2cd64a..ce7a071de3b 100644 --- a/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/devtools_app/macos/Runner.xcodeproj/project.pbxproj @@ -28,8 +28,6 @@ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; - A8DF12F92A961E0BE748392A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */; }; - FBB6B0E13D9A365390189BEF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -63,8 +61,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 275258A68FB221CA96512D9D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -81,15 +77,9 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 492AAD93062A49196993214B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - D36B797FD17AF803B3F3A442 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -97,7 +87,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A8DF12F92A961E0BE748392A /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -106,22 +95,12 @@ buildActionMask = 2147483647; files = ( 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, - FBB6B0E13D9A365390189BEF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 2451268784E41F8223F03594 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 2213BD95695D8AB3EA25555B /* Pods_Runner.framework */, - A5897F3234DE4AE74CCBF51F /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -148,8 +127,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - 59A677840E39A9BEE994C28D /* Pods */, - 2451268784E41F8223F03594 /* Frameworks */, ); sourceTree = ""; }; @@ -198,20 +175,6 @@ path = Runner; sourceTree = ""; }; - 59A677840E39A9BEE994C28D /* Pods */ = { - isa = PBXGroup; - children = ( - D36B797FD17AF803B3F3A442 /* Pods-Runner.debug.xcconfig */, - 275258A68FB221CA96512D9D /* Pods-Runner.release.xcconfig */, - 492AAD93062A49196993214B /* Pods-Runner.profile.xcconfig */, - 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */, - B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */, - BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -219,7 +182,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 81D9EC1F921BB673D2821177 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -238,7 +200,6 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 69D32B8BF13975CC87752C21 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, @@ -369,50 +330,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 69D32B8BF13975CC87752C21 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 81D9EC1F921BB673D2821177 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -464,7 +381,6 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 58B9BF24A1DC8577FE72F028 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -479,7 +395,6 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B830C2735666E24E5089DCE9 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -494,7 +409,6 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BFEA2B4643A0F54CAED9CDFB /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; From 0566b5c2dc1a9195496bcd8183ffae1b3eec0389 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:24:48 -0700 Subject: [PATCH 09/10] Revert "Attempt to fix memory screen goldens" This reverts commit 7b724e09adfa62105f81eb45bc779ccb6ecdbcfb. --- .../memory/framework/memory_screen_test.dart | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart b/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart index 60542996767..79befb2bf5c 100644 --- a/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart +++ b/packages/devtools_app/test/screens/memory/framework/memory_screen_test.dart @@ -63,23 +63,14 @@ void main() { } Future pumpMemoryScreen(WidgetTester tester) async { - await tester.runAsync(() async { - await tester.pumpWidget( - wrapWithControllers(const MemoryScreenBody(), memory: controller), - ); + await tester.pumpWidget( + wrapWithControllers(const MemoryScreenBody(), memory: controller), + ); - // Current workaround for flaky image asset testing. - // See: https://github.com/flutter/flutter/issues/38997 - for (final element in find.byType(Image).evaluate()) { - final widget = element.widget as Image; - final image = widget.image; - await precacheImage(image, element); - await tester.pumpAndSettle(); - } - - // Delay to ensure the memory profiler has collected data. - await tester.pumpAndSettle(const Duration(seconds: 2)); - }); + // Delay to ensure the memory profiler has collected data. + await tester.runAsync( + () async => await tester.pumpAndSettle(const Duration(seconds: 2)), + ); expect(find.byType(MemoryScreenBody), findsOneWidget); } From b9fcc780bc376ba2fa3df0e918f58ce5df8a2bd2 Mon Sep 17 00:00:00 2001 From: Elliott Brooks <21270878+elliette@users.noreply.github.com> Date: Mon, 20 Apr 2026 12:41:22 -0700 Subject: [PATCH 10/10] Fix imports --- .../lib/src/screens/inspector_v2/inspector_screen.dart | 2 +- .../src/screens/inspector_v2/inspector_screen_controller.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 42d5bb7f0ed..1ac60f68bcd 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 @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import '../../shared/framework/screen.dart'; import '../../shared/globals.dart'; -import '../inspector_v2/inspector_screen_body.dart'; +import 'inspector_screen_body.dart'; import 'inspector_screen_controller.dart'; class InspectorScreen extends Screen { diff --git a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_controller.dart b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_controller.dart index 7656c99ae04..5a403dbad44 100644 --- a/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector_v2/inspector_screen_controller.dart @@ -6,8 +6,8 @@ import '../../shared/analytics/metrics.dart'; import '../../shared/console/primitives/simple_items.dart'; import '../../shared/framework/screen.dart'; import '../../shared/framework/screen_controllers.dart'; -import '../inspector_v2/inspector_controller.dart'; -import '../inspector_v2/inspector_tree_controller.dart'; +import 'inspector_controller.dart'; +import 'inspector_tree_controller.dart'; /// Screen controller for the Inspector screen. ///