diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart index 171231eeef8..bf772c4a70c 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_controller.dart @@ -31,6 +31,7 @@ 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/diagnostics/primitives/source_location.dart'; import '../../shared/framework/screen_controllers.dart'; import '../../shared/globals.dart'; import '../../shared/managers/notifications.dart'; @@ -55,6 +56,9 @@ typedef WidgetTreeNodeProperties = ({ /// Layout properties for the widget. LayoutProperties? layoutProperties, + + /// Source location where the selected widget was created. + InspectorSourceLocation? creationLocation, }); /// This class is based on the InspectorPanel class from the Flutter IntelliJ @@ -255,6 +259,7 @@ class InspectorController extends DisposableController widgetProperties: [], renderProperties: [], layoutProperties: null, + creationLocation: null, )); /// Whether the implementation widgets are hidden in the widget tree. @@ -817,6 +822,7 @@ class InspectorController extends DisposableController final widgetProperties = []; final renderProperties = []; LayoutProperties? layoutProperties; + InspectorSourceLocation? creationLocation; final diagnostic = node?.diagnostic; final objectGroupApi = diagnostic?.objectGroupApi; if (diagnostic != null && objectGroupApi != null) { @@ -824,7 +830,7 @@ class InspectorController extends DisposableController // Fetch widget properties: final wProperties = await diagnostic.getProperties(objectGroupApi); // Check if the selected node has changed, and if so return early: - if (_selectedNode.value != node) { + if (disposed || _selectedNode.value != node) { return; } widgetProperties.addAll( @@ -838,11 +844,22 @@ class InspectorController extends DisposableController diagnostic, forFlexLayout: false, ); + // Fetch creation location from the details subtree. Summary tree nodes + // omit creationLocation when loaded with fullDetails: false. + final detailsNode = await objectGroupApi.getDetailsSubtree( + diagnostic, + subtreeDepth: 0, + ); + // Check if the selected node has changed, and if so return early: + if (disposed || _selectedNode.value != node) { + return; + } + creationLocation = detailsNode?.creationLocation; // Fetch RenderObject properties: for (final renderObject in renderProperties) { final rProperties = await renderObject.getProperties(objectGroupApi); // Check if the selected node has changed, and if so return early: - if (_selectedNode.value != node) { + if (disposed || _selectedNode.value != node) { return; } renderProperties.addAll(rProperties); @@ -851,10 +868,12 @@ class InspectorController extends DisposableController _log.warning(e, st); } } + if (disposed) return; _selectedNodeProperties.value = ( widgetProperties: widgetProperties, renderProperties: renderProperties, layoutProperties: layoutProperties, + creationLocation: creationLocation, ); } diff --git a/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart b/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart index 34ec1ac2d57..377b0141b64 100644 --- a/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart +++ b/packages/devtools_app/lib/src/screens/inspector/widget_properties/properties_view.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import '../../../shared/analytics/constants.dart' as gac; import '../../../shared/console/widgets/description.dart'; import '../../../shared/diagnostics/diagnostics_node.dart'; +import '../../../shared/diagnostics/primitives/source_location.dart'; import '../../../shared/primitives/utils.dart'; import '../../../shared/ui/tab.dart'; import '../inspector_controller.dart'; @@ -76,47 +77,56 @@ class _DetailsTableState extends State { final widgetProperties = properties.widgetProperties; final renderProperties = properties.renderProperties; final layoutProperties = properties.layoutProperties; + final creationLocation = properties.creationLocation; final renderTabExists = renderProperties.isNotEmpty; final flexExplorerTabExists = selectedNode?.isFlexLayout ?? false; - return AnalyticsTabbedView( - gaScreen: gac.inspector, - onTabChanged: (int tabIndex) { - _lastSelectedTab = _getTabForIndex( - tabIndex, - renderTabExists: renderTabExists, - flexExplorerTabExists: flexExplorerTabExists, - ); - }, - initialSelectedIndex: _getIndexForTab( - _lastSelectedTab ?? _widgetPropertiesTab, - renderTabExists: renderTabExists, - flexExplorerTabExists: flexExplorerTabExists, - ), - tabs: [ - ( - tab: _widgetPropertiesTab, - tabView: PropertiesView( - properties: widgetProperties, - layoutProperties: layoutProperties, - controller: widget.controller, - scrollController: _widgetPropertiesScrollController, - ), - ), - if (renderTabExists) - ( - tab: _renderObjectTab, - tabView: PropertiesTable( - properties: renderProperties, - scrollController: _renderPropertiesScrollController, + return Column( + children: [ + if (_shouldShowCreationLocation(creationLocation)) + WidgetCreationLocationHeader(location: creationLocation!), + Expanded( + child: AnalyticsTabbedView( + gaScreen: gac.inspector, + onTabChanged: (int tabIndex) { + _lastSelectedTab = _getTabForIndex( + tabIndex, + renderTabExists: renderTabExists, + flexExplorerTabExists: flexExplorerTabExists, + ); + }, + initialSelectedIndex: _getIndexForTab( + _lastSelectedTab ?? _widgetPropertiesTab, + renderTabExists: renderTabExists, + flexExplorerTabExists: flexExplorerTabExists, ), + tabs: [ + ( + tab: _widgetPropertiesTab, + tabView: PropertiesView( + properties: widgetProperties, + layoutProperties: layoutProperties, + controller: widget.controller, + scrollController: _widgetPropertiesScrollController, + ), + ), + if (renderTabExists) + ( + tab: _renderObjectTab, + tabView: PropertiesTable( + properties: renderProperties, + scrollController: _renderPropertiesScrollController, + ), + ), + if (flexExplorerTabExists) + ( + tab: _flexExplorerTab, + tabView: FlexLayoutExplorerWidget(widget.controller), + ), + ], ), - if (flexExplorerTabExists) - ( - tab: _flexExplorerTab, - tabView: FlexLayoutExplorerWidget(widget.controller), - ), + ), ], ); }, @@ -158,6 +168,48 @@ class _DetailsTableState extends State { if (renderTabExists) _renderObjectTab, if (flexExplorerTabExists) _flexExplorerTab, ]; + + bool _shouldShowCreationLocation(InspectorSourceLocation? location) { + return location?.getFile() != null; + } +} + +/// Displays the source file path for the selected widget. +/// +/// Matches the legacy inspector format: `filename.dart:line:column`. +class WidgetCreationLocationHeader extends StatelessWidget { + const WidgetCreationLocationHeader({super.key, required this.location}); + + final InspectorSourceLocation location; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final file = location.getFile(); + final line = location.getLine(); + final column = location.getColumn(); + final shortLocation = '${fileNameFromUri(file)}:$line:$column'; + final fullLocation = '$file:$line:$column'; + + return Container( + width: double.infinity, + height: defaultHeaderHeight, + padding: const EdgeInsets.symmetric(horizontal: denseSpacing), + alignment: Alignment.centerLeft, + decoration: BoxDecoration( + border: Border(bottom: defaultBorderSide(theme)), + ), + child: DevToolsTooltip( + message: fullLocation, + child: Text( + shortLocation, + style: theme.subtleTextStyle, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ); + } } /// Displays a widget's properties, including the layout properties and a diff --git a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart index e9837552292..6081c5d09b5 100644 --- a/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart +++ b/packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart @@ -921,6 +921,24 @@ abstract class InspectorObjectGroupBase ); } + @override + Future getDetailsSubtree( + RemoteDiagnosticsNode? node, { + int subtreeDepth = 2, + }) async { + if (node == null || node.valueRef.id == null) return null; + return parseDiagnosticsNodeDaemon( + invokeServiceMethodDaemonParams( + WidgetInspectorServiceExtensions.getDetailsSubtree.name, + { + 'objectGroup': groupName, + 'arg': node.valueRef.id, + 'subtreeDepth': subtreeDepth.toString(), + }, + ), + ); + } + @override bool isLocalClass(RemoteDiagnosticsNode node) => inspectorService.isLocalClass(node); diff --git a/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart b/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart index b1195a1823a..328d50b4649 100644 --- a/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart +++ b/packages/devtools_app/lib/src/shared/diagnostics/object_group_api.dart @@ -39,4 +39,10 @@ abstract class InspectorObjectGroupApi ); Future> getProperties(InspectorInstanceRef instanceRef); + + /// Returns a details subtree for [node], including creation location data. + /// + /// Pass a small [subtreeDepth] (for example `0`) when only node-level details + /// such as creation location are needed. + Future getDetailsSubtree(T? node, {int subtreeDepth = 2}); } diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 998ed947ab1..7668fcac2f6 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -21,7 +21,10 @@ To learn more about DevTools, check out the ## Inspector updates -TODO: Remove this section if there are not any updates. +* Added the widget source file path to the Inspector details pane + (`filename.dart:line:column`), matching legacy Inspector behavior. - + [#9972](https://github.com/flutter/devtools/pull/9972), + [#9922](https://github.com/flutter/devtools/issues/9922) ## Performance updates diff --git a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart index 47eb4923e4d..3f09b29eb1f 100644 --- a/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart +++ b/packages/devtools_app/test/screens/inspector/inspector_integration_test.dart @@ -708,22 +708,3 @@ Future _resetPubRootDirectories(InspectorService inspectorService) async { await inspectorService.addPubRootDirectories([rootLibrary]); } } - -extension _ObjectGroupTestExtension on ObjectGroup { - Future getDetailsSubtree( - RemoteDiagnosticsNode? node, { - int subtreeDepth = 2, - }) async { - if (node == null) return null; - final args = { - 'objectGroup': groupName, - 'arg': node.valueRef.id, - 'subtreeDepth': subtreeDepth.toString(), - }; - final json = await invokeServiceMethodDaemonParams( - WidgetInspectorServiceExtensions.getDetailsSubtree.name, - args, - ); - return parseDiagnosticsNodeHelper(json as Map?); - } -} diff --git a/packages/devtools_app/test/screens/inspector/widget_creation_location_header_test.dart b/packages/devtools_app/test/screens/inspector/widget_creation_location_header_test.dart new file mode 100644 index 00000000000..c3caa4322a0 --- /dev/null +++ b/packages/devtools_app/test/screens/inspector/widget_creation_location_header_test.dart @@ -0,0 +1,64 @@ +// Copyright 2026 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/widget_properties/properties_view.dart'; +import 'package:devtools_app/src/shared/diagnostics/primitives/source_location.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + setUp(() { + setGlobal(IdeTheme, IdeTheme()); + }); + + Widget wrapHeader(Widget child) { + return MaterialApp( + theme: themeFor( + isDarkTheme: false, + ideTheme: IdeTheme(), + theme: ThemeData(useMaterial3: true, colorScheme: lightColorScheme), + ), + home: Scaffold(body: child), + ); + } + + testWidgets('shows file name with line and column', ( + WidgetTester tester, + ) async { + final location = InspectorSourceLocation({ + 'file': 'file:///Users/prismo/flutter_app/lib/main.dart', + 'line': 109, + 'column': 23, + }, null); + + await tester.pumpWidget( + wrapHeader(WidgetCreationLocationHeader(location: location)), + ); + + expect(find.text('main.dart:109:23'), findsOneWidget); + }); + + testWidgets('tooltip includes the full file URI', ( + WidgetTester tester, + ) async { + final location = InspectorSourceLocation({ + 'file': 'file:///Users/prismo/flutter_app/lib/main.dart', + 'line': 109, + 'column': 23, + }, null); + + await tester.pumpWidget( + wrapHeader(WidgetCreationLocationHeader(location: location)), + ); + + final tooltip = + tester.widget(find.byType(DevToolsTooltip)) as DevToolsTooltip; + expect( + tooltip.message, + 'file:///Users/prismo/flutter_app/lib/main.dart:109:23', + ); + }); +} diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png index d427fa3c69a..600a10545b4 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_1_initial_load.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png index 31bbdf6ac61..2714965b15c 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_errors_2_error_selected.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png index 8dd6dad63df..a2c51d06030 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_collapsed.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png index 8431c626faa..11e228bd2a9 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_implementation_widgets_hidden.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png index 5a29ff42ad7..e8c2b4b62f6 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_initial_load.png differ diff --git a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png index b02d1e0b87e..cae8749a02a 100644 Binary files a/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png and b/packages/devtools_app/test/test_infra/goldens/integration_inspector_select_center.png differ diff --git a/packages/devtools_test/lib/src/mocks/mocks.dart b/packages/devtools_test/lib/src/mocks/mocks.dart index ca204b350f6..c803c7588b5 100644 --- a/packages/devtools_test/lib/src/mocks/mocks.dart +++ b/packages/devtools_test/lib/src/mocks/mocks.dart @@ -81,6 +81,7 @@ class TestInspectorController extends Fake implements InspectorController { widgetProperties: [], renderProperties: [], layoutProperties: null, + creationLocation: null, )); @override