Show Performance and CPU Profiler screens without app connection (#6567)
* Add Performance and CPU Profiler screens to list of statically available screens
* add OpenSaveButtonGroup
* release notes and changelog
* dart doc
* Update packages/devtools_app/lib/src/screens/performance/performance_screen.dart
Co-authored-by: Daniel Chevalier <danchevalier@google.com>
---------
Co-authored-by: Daniel Chevalier <danchevalier@google.com>
diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart
index 2905854..2160835 100644
--- a/packages/devtools_app/lib/src/app.dart
+++ b/packages/devtools_app/lib/src/app.dart
@@ -311,7 +311,7 @@
// hot restart service is available for the connected app.
const HotRestartButton(),
],
- ...DevToolsScaffold.defaultActions(isEmbedded: embed),
+ ...DevToolsScaffold.defaultActions(),
],
),
);
diff --git a/packages/devtools_app/lib/src/framework/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold.dart
index 4249e0e..8c794c1 100644
--- a/packages/devtools_app/lib/src/framework/scaffold.dart
+++ b/packages/devtools_app/lib/src/framework/scaffold.dart
@@ -41,7 +41,7 @@
this.page,
List<Widget>? actions,
this.embed = false,
- }) : actions = actions ?? defaultActions(isEmbedded: embed),
+ }) : actions = actions ?? defaultActions(),
super(key: key);
DevToolsScaffold.withChild({
@@ -56,16 +56,11 @@
embed: embed,
);
- static List<Widget> defaultActions({
- required bool isEmbedded,
- Color? color,
- }) =>
- [
+ static List<Widget> defaultActions({Color? color}) => [
OpenSettingsAction(color: color),
if (FeatureFlags.devToolsExtensions)
ExtensionSettingsAction(color: color),
ReportFeedbackButton(color: color),
- if (!isEmbedded) ImportToolbarAction(color: color),
OpenAboutAction(color: color),
];
diff --git a/packages/devtools_app/lib/src/framework/status_line.dart b/packages/devtools_app/lib/src/framework/status_line.dart
index 37235c7..5e31f37 100644
--- a/packages/devtools_app/lib/src/framework/status_line.dart
+++ b/packages/devtools_app/lib/src/framework/status_line.dart
@@ -113,10 +113,7 @@
BulletSpacer(color: color),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
- children: DevToolsScaffold.defaultActions(
- color: color,
- isEmbedded: isEmbedded,
- ),
+ children: DevToolsScaffold.defaultActions(color: color),
),
],
];
diff --git a/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart
index cbe34d1..2ecc60a 100644
--- a/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart
+++ b/packages/devtools_app/lib/src/screens/performance/panes/controls/performance_controls.dart
@@ -12,7 +12,9 @@
import '../../../../shared/analytics/analytics.dart' as ga;
import '../../../../shared/analytics/constants.dart' as gac;
import '../../../../shared/common_widgets.dart';
+import '../../../../shared/file_import.dart';
import '../../../../shared/globals.dart';
+import '../../../../shared/screen.dart';
import '../../panes/timeline_events/timeline_events_controller.dart';
import '../../performance_controller.dart';
import 'enhance_tracing/enhance_tracing.dart';
@@ -146,12 +148,9 @@
const MoreDebuggingOptionsButton(),
],
const SizedBox(width: denseSpacing),
- GaDevToolsButton.iconOnly(
- icon: Icons.file_download,
- gaScreen: gac.performance,
- gaSelection: gac.export,
- tooltip: 'Export data',
- onPressed: _exportPerformanceData,
+ OpenSaveButtonGroup(
+ screenId: ScreenMetaData.performance.id,
+ onSave: controller.exportData,
),
const SizedBox(width: denseSpacing),
SettingsOutlinedButton(
@@ -163,13 +162,6 @@
);
}
- void _exportPerformanceData() {
- ga.select(gac.performance, gac.export);
- controller.exportData();
- // TODO(kenz): investigate if we need to do any error handling here. Is the
- // download always successful?
- }
-
void _openSettingsDialog(BuildContext context) {
unawaited(
showDialog(
diff --git a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
index d08cf83..eed8a83 100644
--- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
+++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart
@@ -9,11 +9,15 @@
import 'package:devtools_shared/devtools_shared.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../shared/analytics/analytics.dart' as ga;
+import '../../shared/analytics/constants.dart' as gac;
import '../../shared/banner_messages.dart';
import '../../shared/common_widgets.dart';
+import '../../shared/config_specific/import_export/import_export.dart';
+import '../../shared/file_import.dart';
import '../../shared/globals.dart';
import '../../shared/screen.dart';
import '../../shared/utils.dart';
@@ -35,6 +39,12 @@
@override
Widget build(BuildContext context) {
+ final connected = serviceConnection.serviceManager.hasConnection &&
+ serviceConnection.serviceManager.connectedAppInitialized;
+ if (!connected && !offlineController.offlineMode.value) {
+ return const DisconnectedPerformanceScreenBody();
+ }
+
if (serviceConnection.serviceManager.connectedApp?.isDartWebAppNow ??
false) {
return const WebPerformanceScreenBody();
@@ -148,6 +158,28 @@
}
}
+class DisconnectedPerformanceScreenBody extends StatelessWidget {
+ const DisconnectedPerformanceScreenBody({super.key});
+
+ static const importInstructions =
+ 'Open a performance data file that was previously saved from DevTools.';
+
+ @override
+ Widget build(BuildContext context) {
+ return FileImportContainer(
+ instructions: importInstructions,
+ actionText: 'Load data',
+ gaScreen: gac.appSize,
+ gaSelectionImport: gac.PerformanceEvents.openDataFile.name,
+ gaSelectionAction: gac.PerformanceEvents.loadDataFromFile.name,
+ onAction: (jsonFile) {
+ Provider.of<ImportController>(context, listen: false)
+ .importData(jsonFile, expectedScreenId: PerformanceScreen.id);
+ },
+ );
+ }
+}
+
const timelineLink =
'https://api.flutter.dev/flutter/dart-developer/Timeline-class.html';
const timelineTaskLink =
diff --git a/packages/devtools_app/lib/src/screens/profiler/panes/controls/profiler_screen_controls.dart b/packages/devtools_app/lib/src/screens/profiler/panes/controls/profiler_screen_controls.dart
index ead2e93..4b3a006 100644
--- a/packages/devtools_app/lib/src/screens/profiler/panes/controls/profiler_screen_controls.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/panes/controls/profiler_screen_controls.dart
@@ -7,7 +7,9 @@
import '../../../../shared/analytics/constants.dart' as gac;
import '../../../../shared/common_widgets.dart';
+import '../../../../shared/file_import.dart';
import '../../../../shared/globals.dart';
+import '../../../../shared/screen.dart';
import '../../../../shared/ui/vm_flag_widgets.dart';
import '../../profiler_screen_controller.dart';
@@ -107,8 +109,6 @@
required this.profilerBusy,
});
- static const _secondaryControlsMinScreenWidthForText = 1170.0;
-
static const _profilingControlsMinScreenWidthForText = 875.0;
final ProfilerScreenController controller;
@@ -155,15 +155,13 @@
controller.cpuProfilerController.profilePeriodFlag!,
),
const SizedBox(width: denseSpacing),
- ExportButton(
- gaScreen: gac.cpuProfiler,
- onPressed: !profilerBusy &&
+ OpenSaveButtonGroup(
+ screenId: ScreenMetaData.cpuProfiler.id,
+ onSave: !profilerBusy &&
controller.cpuProfileData != null &&
controller.cpuProfileData?.isEmpty == false
? _exportPerformance
: null,
- minScreenWidthForTextBeforeScaling:
- _secondaryControlsMinScreenWidthForText,
),
],
);
diff --git a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart
index efc147b..5146b33 100644
--- a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart
+++ b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart
@@ -6,11 +6,15 @@
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
+import 'package:provider/provider.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/banner_messages.dart';
import '../../shared/common_widgets.dart';
+import '../../shared/config_specific/import_export/import_export.dart';
+import '../../shared/file_import.dart';
import '../../shared/globals.dart';
import '../../shared/primitives/listenable.dart';
import '../../shared/screen.dart';
@@ -35,7 +39,15 @@
const FixedValueListenable<bool>(true);
@override
- Widget build(BuildContext context) => const ProfilerScreenBody();
+ Widget build(BuildContext context) {
+ final connected = serviceConnection.serviceManager.hasConnection &&
+ serviceConnection.serviceManager.connectedAppInitialized;
+ if (!connected && !offlineController.offlineMode.value) {
+ return const DisconnectedCpuProfilerScreenBody();
+ }
+
+ return const ProfilerScreenBody();
+ }
}
class ProfilerScreenBody extends StatefulWidget {
@@ -164,3 +176,25 @@
);
}
}
+
+class DisconnectedCpuProfilerScreenBody extends StatelessWidget {
+ const DisconnectedCpuProfilerScreenBody({super.key});
+
+ static const importInstructions =
+ 'Open a CPU profile that was previously saved from DevTools';
+
+ @override
+ Widget build(BuildContext context) {
+ return FileImportContainer(
+ instructions: importInstructions,
+ actionText: 'Load data',
+ gaScreen: gac.appSize,
+ gaSelectionImport: gac.CpuProfilerEvents.openDataFile.name,
+ gaSelectionAction: gac.CpuProfilerEvents.loadDataFromFile.name,
+ onAction: (jsonFile) {
+ Provider.of<ImportController>(context, listen: false)
+ .importData(jsonFile, expectedScreenId: ProfilerScreen.id);
+ },
+ );
+ }
+}
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart
index c51c3d5..4546930 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants.dart
@@ -129,7 +129,8 @@
const clear = 'clear';
const record = 'record';
const stop = 'stop';
-const export = 'export';
+const openFile = 'openFile';
+const saveFile = 'saveFile';
const expandAll = 'expandAll';
const collapseAll = 'collapseAll';
const profileModeDocs = 'profileModeDocs';
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_cpu_profiler_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_cpu_profiler_constants.dart
index 4784183..01ffbff 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants/_cpu_profiler_constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants/_cpu_profiler_constants.dart
@@ -11,6 +11,8 @@
cpuProfileFlameChartHelp,
cpuProfileProcessingTime,
cpuProfileDisplayTreeGuidelines,
+ openDataFile,
+ loadDataFromFile;
}
enum CpuProfilerDocs {
diff --git a/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart
index 5336fee..ceaf8d9 100644
--- a/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart
+++ b/packages/devtools_app/lib/src/shared/analytics/constants/_performance_constants.dart
@@ -29,7 +29,9 @@
perfettoScrollToTimeRange,
perfettoShowHelp,
performanceSettings,
- traceCategories;
+ traceCategories,
+ openDataFile,
+ loadDataFromFile;
const PerformanceEvents([this.nameOverride]);
diff --git a/packages/devtools_app/lib/src/shared/common_widgets.dart b/packages/devtools_app/lib/src/shared/common_widgets.dart
index 67eb428..c16bfd4 100644
--- a/packages/devtools_app/lib/src/shared/common_widgets.dart
+++ b/packages/devtools_app/lib/src/shared/common_widgets.dart
@@ -704,25 +704,6 @@
Size get preferredSize => Size.zero;
}
-/// Button to export data.
-///
-/// * `minScreenWidthForTextBeforeScaling`: The minimum width the button can be before the text is
-/// omitted.
-/// * `onPressed`: The callback to be called upon pressing the button.
-class ExportButton extends GaDevToolsButton {
- ExportButton({
- required super.gaScreen,
- super.key,
- super.onPressed,
- super.minScreenWidthForTextBeforeScaling,
- super.tooltip = 'Export data',
- }) : super(
- icon: Icons.file_download,
- label: 'Export',
- gaSelection: gac.export,
- );
-}
-
/// Button to open related information / documentation.
///
/// [tooltip] specifies the hover text for the button.
diff --git a/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart b/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart
index d8f8f0d..34e5cdf 100644
--- a/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart
+++ b/packages/devtools_app/lib/src/shared/config_specific/import_export/import_export.dart
@@ -2,19 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:async';
import 'dart:convert';
import 'package:devtools_app_shared/service.dart';
-import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
-import 'package:provider/provider.dart';
import '../../../../devtools.dart';
-import '../../analytics/analytics.dart' as ga;
-import '../../analytics/constants.dart' as gac;
-import '../../common_widgets.dart';
-import '../../file_import.dart';
import '../../globals.dart';
import '../../primitives/simple_items.dart';
import '../../primitives/utils.dart';
@@ -42,7 +35,6 @@
activeScreenId,
}
-// TODO(kenz): we should support a file picker import for desktop.
class ImportController {
ImportController(
this._pushSnapshotScreenForImport,
@@ -55,7 +47,7 @@
DateTime? previousImportTime;
// TODO(kenz): improve error handling here or in snapshot_screen.dart.
- void importData(DevToolsJsonFile jsonFile) {
+ void importData(DevToolsJsonFile jsonFile, {String? expectedScreenId}) {
final json = jsonFile.data;
// Do not allow two different imports within 500 ms of each other. This is a
@@ -81,6 +73,14 @@
// TODO(kenz): support imports for more than one screen at a time.
final activeScreenId =
devToolsSnapshot[DevToolsExportKeys.activeScreenId.name];
+ if (expectedScreenId != null && activeScreenId != expectedScreenId) {
+ notificationService.push(
+ 'Expected a data file for screen \'$expectedScreenId\' but received one'
+ ' for screen \'$activeScreenId\'. Please open a file for screen \'$expectedScreenId\'.',
+ );
+ return;
+ }
+
final connectedApp =
(devToolsSnapshot[DevToolsExportKeys.connectedApp.name] ??
<String, Object>{})
@@ -185,29 +185,3 @@
return jsonEncode(data);
}
}
-
-class ImportToolbarAction extends ScaffoldAction {
- ImportToolbarAction({super.key, Color? color})
- : super(
- icon: Icons.upload_rounded,
- tooltip: 'Load data for viewing in DevTools.',
- color: color,
- onPressed: (context) => unawaited(_importFile(context)),
- );
-
- static Future<void> _importFile(BuildContext context) async {
- ga.select(
- gac.devToolsMain,
- gac.importFile,
- );
- final DevToolsJsonFile? importedFile = await importFileFromPicker(
- acceptedTypes: ['json'],
- );
-
- if (importedFile != null) {
- // ignore: use_build_context_synchronously, by design
- Provider.of<ImportController>(context, listen: false)
- .importData(importedFile);
- }
- }
-}
diff --git a/packages/devtools_app/lib/src/shared/file_import.dart b/packages/devtools_app/lib/src/shared/file_import.dart
index 2ae18dd..88dadf8 100644
--- a/packages/devtools_app/lib/src/shared/file_import.dart
+++ b/packages/devtools_app/lib/src/shared/file_import.dart
@@ -7,18 +7,70 @@
import 'package:devtools_app_shared/ui.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'analytics/analytics.dart' as ga;
+import 'analytics/constants.dart' as gac;
import 'common_widgets.dart';
import 'config_specific/drag_and_drop/drag_and_drop.dart';
+import 'config_specific/import_export/import_export.dart';
import 'globals.dart';
import 'primitives/utils.dart';
+class OpenSaveButtonGroup extends StatelessWidget {
+ const OpenSaveButtonGroup({
+ super.key,
+ required this.screenId,
+ required this.onSave,
+ });
+
+ final String screenId;
+
+ final VoidCallback? onSave;
+
+ @override
+ Widget build(BuildContext context) {
+ return RoundedButtonGroup(
+ items: [
+ ButtonGroupItemData(
+ icon: Icons.file_upload,
+ tooltip: 'Open a file that was previously saved from DevTools',
+ onPressed: () async {
+ ga.select(screenId, gac.openFile);
+ final importedFile =
+ await importFileFromPicker(acceptedTypes: const ['json']);
+ if (importedFile != null) {
+ // ignore: use_build_context_synchronously, intentional use.
+ Provider.of<ImportController>(context, listen: false)
+ .importData(importedFile, expectedScreenId: screenId);
+ } else {
+ notificationService.push(
+ 'Something went wrong. Could not open selected file.',
+ );
+ }
+ },
+ ),
+ ButtonGroupItemData(
+ icon: Icons.file_download,
+ tooltip: 'Save this screen\'s data for offline viewing',
+ onPressed: onSave != null
+ ? () {
+ ga.select(screenId, gac.saveFile);
+ onSave!.call();
+ }
+ : null,
+ ),
+ ],
+ );
+ }
+}
+
class FileImportContainer extends StatefulWidget {
const FileImportContainer({
- required this.title,
required this.instructions,
required this.gaScreen,
required this.gaSelectionImport,
+ this.title,
this.gaSelectionAction,
this.actionText,
this.onAction,
@@ -28,7 +80,7 @@
super.key,
});
- final String title;
+ final String? title;
final String instructions;
@@ -59,13 +111,16 @@
@override
Widget build(BuildContext context) {
+ final title = widget.title;
return Column(
children: [
- Text(
- widget.title,
- style: TextStyle(fontSize: scaleByFontFactor(18.0)),
- ),
- const SizedBox(height: defaultSpacing),
+ if (title != null) ...[
+ Text(
+ title,
+ style: TextStyle(fontSize: scaleByFontFactor(18.0)),
+ ),
+ const SizedBox(height: defaultSpacing),
+ ],
Expanded(
// TODO(kenz): improve drag over highlight.
child: DragAndDrop(
@@ -165,7 +220,6 @@
gaScreen: widget.gaScreen,
gaSelection: widget.gaSelectionAction!,
label: widget.actionText!,
- icon: Icons.highlight,
elevated: true,
onPressed: importedFile != null
? () => widget.onAction!(importedFile!)
@@ -248,7 +302,7 @@
return GaDevToolsButton(
onPressed: onPressed,
icon: Icons.file_upload,
- label: 'Import File',
+ label: 'Open file',
gaScreen: gaScreen,
gaSelection: gaSelection,
elevated: elevatedButton,
diff --git a/packages/devtools_app/lib/src/shared/screen.dart b/packages/devtools_app/lib/src/shared/screen.dart
index 3e662c8..fdc3922 100644
--- a/packages/devtools_app/lib/src/shared/screen.dart
+++ b/packages/devtools_app/lib/src/shared/screen.dart
@@ -37,6 +37,7 @@
title: 'Performance',
icon: Octicons.pulse,
worksOffline: true,
+ requiresConnection: false,
tutorialVideoTimestamp: '?t=261',
),
cpuProfiler(
@@ -45,6 +46,7 @@
icon: Octicons.dashboard,
requiresDartVm: true,
worksOffline: true,
+ requiresConnection: false,
tutorialVideoTimestamp: '?t=340',
),
memory(
diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
index 01f51fc..9d08ee5 100644
--- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
+++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
@@ -32,9 +32,18 @@

+* Made the Performance screen available when there is no connected app. Performance data that was
+previously saved from DevTools can be reloaded for viewing from this screen. [#6567](https://github.com/flutter/devtools/pull/6567)
+* Added an "Open" button to the Performance controls for loading data that was previously saved
+from DevTools. [#6567](https://github.com/flutter/devtools/pull/6567)
+
## CPU profiler updates
* Tree guidelines are now always enabled for the "Bottom Up" and "Call Tree" tabs. [#6534](https://github.com/flutter/devtools/pull/6534)
+* Made the CPU profiler screen available when there is no connected app. CPU profiles that were
+previously saved from DevTools can be reloaded for viewing from this screen. [#6567](https://github.com/flutter/devtools/pull/6567)
+* Added an "Open" button to the CPU profiler controls for loading data that was previously saved
+from DevTools. [#6567](https://github.com/flutter/devtools/pull/6567)
## Memory updates
diff --git a/packages/devtools_app/test/cpu_profiler/profiler_screen_test.dart b/packages/devtools_app/test/cpu_profiler/profiler_screen_test.dart
index 120df12..adc6e25 100644
--- a/packages/devtools_app/test/cpu_profiler/profiler_screen_test.dart
+++ b/packages/devtools_app/test/cpu_profiler/profiler_screen_test.dart
@@ -7,6 +7,7 @@
import 'package:devtools_app/src/screens/profiler/panes/controls/cpu_profiler_controls.dart';
import 'package:devtools_app/src/screens/profiler/profiler_status.dart';
import 'package:devtools_app/src/service/vm_flags.dart' as vm_flags;
+import 'package:devtools_app/src/shared/file_import.dart';
import 'package:devtools_app/src/shared/ui/vm_flag_widgets.dart';
import 'package:devtools_test/devtools_test.dart';
import 'package:flutter/material.dart';
@@ -38,6 +39,7 @@
expect(find.text('Profile app start up'), findsOneWidget);
}
expect(find.byType(CpuSamplingRateDropdown), findsOneWidget);
+ expect(find.byType(OpenSaveButtonGroup), findsOneWidget);
expect(
find.byType(ProfileRecordingInstructions),
findsOneWidget,
@@ -144,6 +146,7 @@
expect(find.byType(StopRecordingButton), findsNothing);
expect(find.byType(ClearButton), findsNothing);
expect(find.byType(CpuSamplingRateDropdown), findsNothing);
+ expect(find.byType(OpenSaveButtonGroup), findsNothing);
await tester.runAsync(() async {
await tester.tap(find.text('Enable profiler'));
diff --git a/packages/devtools_app/test/performance/controls/performance_controls_test.dart b/packages/devtools_app/test/performance/controls/performance_controls_test.dart
index f7fa2b5..ff7b261 100644
--- a/packages/devtools_app/test/performance/controls/performance_controls_test.dart
+++ b/packages/devtools_app/test/performance/controls/performance_controls_test.dart
@@ -4,6 +4,7 @@
import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/screens/performance/panes/controls/performance_controls.dart';
+import 'package:devtools_app/src/shared/file_import.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_test/devtools_test.dart';
@@ -76,7 +77,7 @@
expect(find.text('Performance Overlay'), findsOneWidget);
expect(find.text('Enhance Tracing'), findsOneWidget);
expect(find.text('More debugging options'), findsOneWidget);
- expect(find.byIcon(Icons.file_download), findsOneWidget);
+ expect(find.byType(OpenSaveButtonGroup), findsOneWidget);
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
},
);
@@ -99,7 +100,7 @@
expect(find.text('Performance Overlay'), findsNothing);
expect(find.text('Enhance Tracing'), findsNothing);
expect(find.text('More debugging options'), findsNothing);
- expect(find.byIcon(Icons.file_download), findsOneWidget);
+ expect(find.byType(OpenSaveButtonGroup), findsOneWidget);
expect(find.byIcon(Icons.settings_outlined), findsOneWidget);
},
);
@@ -118,7 +119,7 @@
expect(find.text('Performance Overlay'), findsNothing);
expect(find.text('Enhance Tracing'), findsNothing);
expect(find.text('More debugging options'), findsNothing);
- expect(find.byIcon(Icons.file_download), findsNothing);
+ expect(find.byType(OpenSaveButtonGroup), findsNothing);
expect(find.byIcon(Icons.settings_outlined), findsNothing);
offlineController.exitOfflineMode();
},
diff --git a/packages/devtools_app_shared/CHANGELOG.md b/packages/devtools_app_shared/CHANGELOG.md
index bb98535..476d802 100644
--- a/packages/devtools_app_shared/CHANGELOG.md
+++ b/packages/devtools_app_shared/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 0.0.7
+* Add `RoundedButtonGroup` common widget.
+
## 0.0.6
* Add `profilePlatformChannels` to known service extensions.
* Fix a bug where service extension states were not getting cleared on app disconnect.
diff --git a/packages/devtools_app_shared/lib/src/ui/common.dart b/packages/devtools_app_shared/lib/src/ui/common.dart
index 83e45f2..adb1b97 100644
--- a/packages/devtools_app_shared/lib/src/ui/common.dart
+++ b/packages/devtools_app_shared/lib/src/ui/common.dart
@@ -566,6 +566,117 @@
}
}
+/// A group of buttons that share a common border.
+///
+/// This widget ensures the buttons are displayed with proper borders on the
+/// interior and exterior of the group. The attirbutes for each button can be
+/// defined by [ButtonGroupItemData] and included in [items].
+final class RoundedButtonGroup extends StatelessWidget {
+ const RoundedButtonGroup({
+ super.key,
+ required this.items,
+ this.minScreenWidthForTextBeforeScaling,
+ });
+
+ final List<ButtonGroupItemData> items;
+ final double? minScreenWidthForTextBeforeScaling;
+
+ @override
+ Widget build(BuildContext context) {
+ Widget buildButton(int index) {
+ final itemData = items[index];
+ Widget button = _ButtonGroupButton(
+ buttonData: itemData,
+ roundedLeftBorder: index == 0,
+ roundedRightBorder: index == items.length - 1,
+ minScreenWidthForTextBeforeScaling: minScreenWidthForTextBeforeScaling,
+ );
+ if (index != 0) {
+ button = Container(
+ decoration: BoxDecoration(
+ border: Border(
+ left: BorderSide(
+ color: Theme.of(context).focusColor,
+ ),
+ ),
+ ),
+ child: button,
+ );
+ }
+ return button;
+ }
+
+ return SizedBox(
+ height: defaultButtonHeight,
+ child: RoundedOutlinedBorder(
+ child: Row(
+ children: [
+ for (int i = 0; i < items.length; i++) buildButton(i),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+final class _ButtonGroupButton extends StatelessWidget {
+ const _ButtonGroupButton({
+ required this.buttonData,
+ this.roundedLeftBorder = false,
+ this.roundedRightBorder = false,
+ this.minScreenWidthForTextBeforeScaling,
+ });
+
+ final ButtonGroupItemData buttonData;
+ final bool roundedLeftBorder;
+ final bool roundedRightBorder;
+ final double? minScreenWidthForTextBeforeScaling;
+
+ @override
+ Widget build(BuildContext context) {
+ return DevToolsTooltip(
+ message: buttonData.tooltip,
+ child: OutlinedButton(
+ autofocus: buttonData.autofocus,
+ style: OutlinedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(horizontal: densePadding),
+ side: BorderSide.none,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.horizontal(
+ left: roundedLeftBorder ? defaultRadius : Radius.zero,
+ right: roundedRightBorder ? defaultRadius : Radius.zero,
+ ),
+ ),
+ ),
+ onPressed: buttonData.onPressed,
+ child: MaterialIconLabel(
+ label: buttonData.label,
+ iconData: buttonData.icon,
+ minScreenWidthForTextBeforeScaling:
+ minScreenWidthForTextBeforeScaling,
+ ),
+ ),
+ );
+ }
+}
+
+final class ButtonGroupItemData {
+ const ButtonGroupItemData({
+ this.label,
+ this.icon,
+ String? tooltip,
+ this.onPressed,
+ this.autofocus = false,
+ }) : tooltip = tooltip ?? label,
+ assert(label != null || icon != null);
+
+ final String? label;
+ final IconData? icon;
+ final String? tooltip;
+ final VoidCallback? onPressed;
+ final bool autofocus;
+}
+
final class DevToolsFilterButton extends StatelessWidget {
const DevToolsFilterButton({
Key? key,
diff --git a/packages/devtools_app_shared/pubspec.yaml b/packages/devtools_app_shared/pubspec.yaml
index 99a86f7..16ad65d 100644
--- a/packages/devtools_app_shared/pubspec.yaml
+++ b/packages/devtools_app_shared/pubspec.yaml
@@ -1,6 +1,6 @@
name: devtools_app_shared
description: Package of Dart & Flutter structures shared between devtools_app and devtools extensions.
-version: 0.0.6
+version: 0.0.7
repository: https://github.com/flutter/devtools/tree/master/packages/devtools_extensions
environment: