From 839c770eb0188d42afd9611c9ef7c3d0b9b01365 Mon Sep 17 00:00:00 2001 From: Aadeesh-01 Date: Sat, 31 Oct 2026 22:52:58 +0530 Subject: [PATCH 1/2] Add a platform override dropdown to the Inspector toolbar --- .../screens/inspector/inspector_controls.dart | 2 + .../service/service_extension_widgets.dart | 108 ++++++++++++++++++ .../lib/src/service/service_extensions.dart | 10 +- .../release_notes/NEXT_RELEASE_NOTES.md | 3 +- .../inspector/inspector_screen_test.dart | 19 +++ .../service_extension_widgets_test.dart | 61 ++++++++++ packages/devtools_app_shared/CHANGELOG.md | 1 + .../lib/src/service/service_extensions.dart | 2 +- 8 files changed, 195 insertions(+), 11 deletions(-) diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart index 951a93f7040..0969ef21d47 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_controls.dart @@ -72,6 +72,8 @@ class InspectorServiceExtensionButtonGroup extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ + const PlatformOverrideDropdown(), + const SizedBox(width: defaultSpacing), ServiceExtensionButtonGroup( fillColor: Theme.of(context).colorScheme.activeToggleButtonColor, minScreenWidthForText: serviceExtensionButtonsIncludeTextWidth, diff --git a/packages/devtools_app/lib/src/service/service_extension_widgets.dart b/packages/devtools_app/lib/src/service/service_extension_widgets.dart index 07d8fd3086f..024eaa4bf9b 100644 --- a/packages/devtools_app/lib/src/service/service_extension_widgets.dart +++ b/packages/devtools_app/lib/src/service/service_extension_widgets.dart @@ -24,6 +24,7 @@ import '../shared/globals.dart'; import '../shared/primitives/message_bus.dart'; import '../shared/primitives/utils.dart'; import '../shared/ui/common_widgets.dart'; +import '../shared/ui/drop_down_button.dart'; import '../shared/ui/hover.dart'; import 'service_extensions.dart'; import 'service_registrations.dart'; @@ -1215,3 +1216,110 @@ class TogglePlatformSelector extends ServiceExtensionSelector { } */ + +/// Dropdown that overrides the target platform of the connected Flutter app +/// using the [togglePlatformMode] service extension. +/// +/// Hidden when the connected app does not expose the service extension, for +/// example when it runs in profile or release mode. +class PlatformOverrideDropdown extends StatelessWidget { + const PlatformOverrideDropdown({super.key}); + + @visibleForTesting + static const dropdownKey = Key('PlatformOverrideDropdown DropdownButton'); + + static const _fuchsia = 'fuchsia'; + + @override + Widget build(BuildContext context) { + final extensionManager = + serviceConnection.serviceManager.serviceExtensionManager; + final extensionName = togglePlatformMode.extension; + return ValueListenableBuilder( + valueListenable: extensionManager.hasServiceExtension(extensionName), + builder: (context, available, _) { + if (!available) return const SizedBox.shrink(); + return ValueListenableBuilder( + valueListenable: extensionManager.getServiceExtensionState( + extensionName, + ), + builder: (context, state, _) { + // The value is null until DevTools has read it from the app. + final current = state.value as String?; + return AnalyticsDropDownButton( + key: dropdownKey, + gaScreen: gac.inspector, + gaDropDownId: gac.togglePlatform, + message: togglePlatformMode.tooltip, + isDense: true, + value: current, + items: [ + for (final platform in _platformsToShow(current)) + _buildMenuItem(platform), + ], + onChanged: _onPlatformChanged, + ); + }, + ); + }, + ); + } + + /// The platforms to list in the menu. + /// + /// This must include [current], or [DropdownButton] throws. + List _platformsToShow(String? current) { + final platforms = togglePlatformMode.values; + return [ + // Only list Fuchsia when the app is already using it. + for (final platform in platforms) + if (platform != _fuchsia || platform == current) platform, + // The app can report a platform that DevTools does not list yet, such as + // one added in a newer version of Flutter. + if (current != null && !platforms.contains(current)) current, + ]; + } + + ({DropdownMenuItem item, String gaId}) _buildMenuItem( + String platform, + ) { + final index = togglePlatformMode.values.indexOf(platform); + final label = index == -1 + ? platform + : togglePlatformMode.displayValues[index]; + return ( + item: DropdownMenuItem( + value: platform, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(_iconFor(platform), size: defaultIconSize), + const SizedBox(width: densePadding), + Text(label), + ], + ), + ), + gaId: platform, + ); + } + + static IconData _iconFor(String platform) => switch (platform) { + 'android' => Icons.android, + 'iOS' => Icons.phone_iphone, + 'macOS' => Icons.laptop_mac, + 'windows' => Icons.desktop_windows, + _ => Icons.computer, + }; + + void _onPlatformChanged(String? platform) { + if (platform == null) return; + unawaited( + serviceConnection.serviceManager.serviceExtensionManager + .setServiceExtensionState( + togglePlatformMode.extension, + enabled: true, + value: platform, + ), + ); + } +} diff --git a/packages/devtools_app/lib/src/service/service_extensions.dart b/packages/devtools_app/lib/src/service/service_extensions.dart index 4057d9499ee..4e142838697 100644 --- a/packages/devtools_app/lib/src/service/service_extensions.dart +++ b/packages/devtools_app/lib/src/service/service_extensions.dart @@ -354,19 +354,11 @@ final slowAnimations = ToggleableServiceExtensionDescription.from( gaDocsItem: gac.slowAnimationDocs, ); -// TODO(https://github.com/flutter/devtools/issues/2780): hook this up in the UI. -// ignore: unused-code, TODO(https://github.com/flutter/devtools/issues/2780). final togglePlatformMode = ServiceExtensionDescription.from( extensions.togglePlatformMode, title: 'Override target platform', iconAsset: 'icons/phone@2x.png', - displayValues: [ - 'Platform: iOS', - 'Platform: Android', - 'Platform: Fuchsia', - 'Platform: MacOS', - 'Platform: Linux', - ], + displayValues: ['iOS', 'Android', 'Fuchsia', 'macOS', 'Linux', 'Windows'], gaScreenName: gac.inspector, gaItem: gac.togglePlatform, tooltip: 'Override Target Platform', diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 7e9c5050340..a01e61584b3 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -20,7 +20,8 @@ the `package:provider` DevTools extension. [#10046](https://github.com/flutter/d ## Inspector updates -TODO: Remove this section if there are not any updates. +* Added a dropdown to the Inspector toolbar for overriding the target + platform of the connected app. [#PR_NUMBER](https://github.com/flutter/devtools/pull/PR_NUMBER) ## Performance updates diff --git a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart index a33a076cd8d..136a15b5985 100644 --- a/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart +++ b/packages/devtools_app/test/screens/inspector/inspector_screen_test.dart @@ -11,6 +11,7 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/inspector/inspector_settings_dialog.dart'; import 'package:devtools_app/src/screens/inspector/layout_explorer/flex/flex.dart'; import 'package:devtools_app/src/screens/inspector/widget_details.dart'; +import 'package:devtools_app/src/service/service_extension_widgets.dart'; import 'package:devtools_app/src/service/service_extensions.dart' as extensions; import 'package:devtools_app/src/shared/feature_flags.dart'; import 'package:devtools_app/src/shared/ui/tab.dart'; @@ -256,6 +257,24 @@ void main() { }, ); + testWidgetsWithWindowSize( + 'shows the platform override dropdown', + windowSize, + (WidgetTester tester) async { + final platformExtension = extensions.togglePlatformMode.extension; + fakeExtensionManager.extensionValueOnDevice = { + platformExtension: 'android', + }; + await fakeExtensionManager.fakeAddServiceExtension(platformExtension); + await fakeExtensionManager.fakeFrame(); + + await tester.pumpWidget(buildInspectorScreen()); + await tester.pump(); + expect(find.byKey(PlatformOverrideDropdown.dropdownKey), findsOneWidget); + expect(find.text('Android'), findsOneWidget); + }, + ); + group('LayoutDetailsTab', () { final renderObjectJson = jsonDecode(''' { diff --git a/packages/devtools_app/test/service/service_extension_widgets_test.dart b/packages/devtools_app/test/service/service_extension_widgets_test.dart index 236a5cafc4c..bc5c82585c7 100644 --- a/packages/devtools_app/test/service/service_extension_widgets_test.dart +++ b/packages/devtools_app/test/service/service_extension_widgets_test.dart @@ -253,6 +253,67 @@ void main() { expect(checkboxWidget.onChanged, isNull); }); }); + + group('PlatformOverrideDropdown', () { + final platformExtension = extensions.togglePlatformMode.extension; + late FakeServiceExtensionManager fakeExtensionManager; + + setUp(() async { + fakeExtensionManager = + mockServiceManager.serviceExtensionManager + as FakeServiceExtensionManager; + await fakeExtensionManager.fakeFrame(); + }); + + Future pumpDropdown(WidgetTester tester) async { + await tester.pumpWidget( + wrap(const Scaffold(body: Center(child: PlatformOverrideDropdown()))), + ); + } + + testWidgets('overrides the platform of the connected app', ( + WidgetTester tester, + ) async { + fakeExtensionManager.extensionValueOnDevice = { + platformExtension: 'android', + }; + await fakeExtensionManager.fakeAddServiceExtension(platformExtension); + await pumpDropdown(tester); + expect(find.text('Android'), findsOneWidget); + + await tester.tap(find.byKey(PlatformOverrideDropdown.dropdownKey)); + await tester.pumpAndSettle(); // finish the menu animation + expect(find.text('Fuchsia'), findsNothing); + expect(find.text('Windows'), findsOneWidget); + // The open menu shows each item again, so tap the last match. + await tester.tap(find.text('iOS').last); + await tester.pumpAndSettle(); // finish the menu animation + + expect( + fakeExtensionManager.extensionValueOnDevice[platformExtension], + 'iOS', + ); + expect(find.text('iOS'), findsOneWidget); + }); + + testWidgets('lists a platform that DevTools does not know about', ( + WidgetTester tester, + ) async { + fakeExtensionManager.extensionValueOnDevice = { + platformExtension: 'newPlatform', + }; + await fakeExtensionManager.fakeAddServiceExtension(platformExtension); + await pumpDropdown(tester); + expect(find.text('newPlatform'), findsOneWidget); + }); + + testWidgets('is hidden when the service extension is not available', ( + WidgetTester tester, + ) async { + await pumpDropdown(tester); + expect(find.byKey(PlatformOverrideDropdown.dropdownKey), findsNothing); + }); + }); } base class _UnavailableServiceExtensionManager diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md index 683bc2bdaf9..f1b1716ef24 100644 --- a/packages/devtools_app_shared/CHANGELOG.md +++ b/packages/devtools_app_shared/CHANGELOG.md @@ -14,6 +14,7 @@ found in the LICENSE file or at https://developers.google.com/open-source/licens such as the `file:` URI of a Dart CLI app started with `dart run`. Previously it was the scheme of the URI, such as `file:`, which is not a package. * Updates `devtools_shared` constraint to `^14.0.1`. +* Add `windows` to the values of the `togglePlatformMode` service extension. ## 0.5.1 * Add DevTools-styled text field `DevToolsTextField`. diff --git a/packages/devtools_app_shared/lib/src/service/service_extensions.dart b/packages/devtools_app_shared/lib/src/service/service_extensions.dart index 89d75f118d5..d99f3bb2aeb 100644 --- a/packages/devtools_app_shared/lib/src/service/service_extensions.dart +++ b/packages/devtools_app_shared/lib/src/service/service_extensions.dart @@ -198,7 +198,7 @@ final toggleOnDeviceWidgetInspector = ToggleableServiceExtension( final togglePlatformMode = ServiceExtension( extension: '$flutterExtensionPrefix${FoundationServiceExtensions.platformOverride.name}', - values: ['iOS', 'android', 'fuchsia', 'macOS', 'linux'], + values: ['iOS', 'android', 'fuchsia', 'macOS', 'linux', 'windows'], ); /// Override the platform brightness (light mode, dark mode, or system default). From 3eb28880af29031a9fd82e2a8ead67dfa9c891e2 Mon Sep 17 00:00:00 2001 From: Aadeesh-01 Date: Sat, 31 Oct 2026 23:30:34 +0530 Subject: [PATCH 2/2] Add PR number to release note --- packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 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 a01e61584b3..c3611d16428 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -21,7 +21,7 @@ the `package:provider` DevTools extension. [#10046](https://github.com/flutter/d ## Inspector updates * Added a dropdown to the Inspector toolbar for overriding the target - platform of the connected app. [#PR_NUMBER](https://github.com/flutter/devtools/pull/PR_NUMBER) + platform of the connected app. [#10061](https://github.com/flutter/devtools/pull/10061) ## Performance updates