Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -255,6 +259,7 @@ class InspectorController extends DisposableController
widgetProperties: [],
renderProperties: [],
layoutProperties: null,
creationLocation: null,
));

/// Whether the implementation widgets are hidden in the widget tree.
Expand Down Expand Up @@ -817,14 +822,15 @@ class InspectorController extends DisposableController
final widgetProperties = <RemoteDiagnosticsNode>[];
final renderProperties = <RemoteDiagnosticsNode>[];
LayoutProperties? layoutProperties;
InspectorSourceLocation? creationLocation;
final diagnostic = node?.diagnostic;
final objectGroupApi = diagnostic?.objectGroupApi;
if (diagnostic != null && objectGroupApi != null) {
try {
// 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(
Expand All @@ -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);
Expand All @@ -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,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -76,47 +77,56 @@ class _DetailsTableState extends State<DetailsTable> {
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),
),
),
],
);
},
Expand Down Expand Up @@ -158,6 +168,48 @@ class _DetailsTableState extends State<DetailsTable> {
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';
Comment thread
khanak0509 marked this conversation as resolved.
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,24 @@ abstract class InspectorObjectGroupBase
);
}

@override
Future<RemoteDiagnosticsNode?> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,10 @@ abstract class InspectorObjectGroupApi<T extends DiagnosticableTree>
);

Future<List<T>> 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<T?> getDetailsSubtree(T? node, {int subtreeDepth = 2});
}
5 changes: 4 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,22 +708,3 @@ Future<void> _resetPubRootDirectories(InspectorService inspectorService) async {
await inspectorService.addPubRootDirectories([rootLibrary]);
}
}

extension _ObjectGroupTestExtension on ObjectGroup {
Future<RemoteDiagnosticsNode?> 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<String, Object?>?);
}
}
Original file line number Diff line number Diff line change
@@ -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',
);
});
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/devtools_test/lib/src/mocks/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class TestInspectorController extends Fake implements InspectorController {
widgetProperties: [],
renderProperties: [],
layoutProperties: null,
creationLocation: null,
));

@override
Expand Down