diff --git a/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart b/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart index 054968890c5..7687726eef3 100644 --- a/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart +++ b/packages/devtools_app/integration_test/test/live_connection/eval_and_browse_test.dart @@ -8,14 +8,11 @@ // ignore_for_file: avoid_print +import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/screens/memory/panes/control/primary_controls.dart'; import 'package:devtools_app/src/screens/memory/panes/diff/widgets/snapshot_list.dart'; import 'package:devtools_app/src/screens/memory/shared/primitives/instance_context_menu.dart'; -import 'package:devtools_app/src/shared/banner_messages.dart'; -import 'package:devtools_app/src/shared/common_widgets.dart'; import 'package:devtools_app/src/shared/console/widgets/console_pane.dart'; -import 'package:devtools_app/src/shared/primitives/simple_items.dart'; -import 'package:devtools_app/src/shared/ui/search.dart'; import 'package:devtools_test/devtools_integration_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -43,12 +40,11 @@ void main() { await pumpAndConnectDevTools(tester, testApp); final evalTester = _EvalAndBrowseTester(tester); + await evalTester.prepareMemoryUI(); await _testBasicEval(evalTester); await _testAssignment(evalTester); - await evalTester.switchToMemoryAndIncreaseEval(); - await _profileOneInstance(evalTester); await _profileAllInstances(evalTester); @@ -154,7 +150,9 @@ class _EvalAndBrowseTester { await tester.pump(longPumpDuration); } - Future switchToMemoryAndIncreaseEval() async { + /// Prepares the UI of the memory screen so that the eval-related elements are + /// visible on the screen for testing. + Future prepareMemoryUI() async { // Open memory screen. await switchToScreen(tester, ScreenMetaData.memory); @@ -208,7 +206,7 @@ class _EvalAndBrowseTester { Finder? next, }) async { Future action(int tryNumber) async { - logStatus('tapping #$tryNumber to find \n[$finder]\n'); + logStatus('attempt #$tryNumber, tapping \n[$finder]\n'); tryNumber++; await tester.tap(finder); await tester.pump(duration); diff --git a/packages/devtools_app/integration_test/test/live_connection/service_connection_test.dart b/packages/devtools_app/integration_test/test/live_connection/service_connection_test.dart index 75093899672..7bb931bfbb7 100644 --- a/packages/devtools_app/integration_test/test/live_connection/service_connection_test.dart +++ b/packages/devtools_app/integration_test/test/live_connection/service_connection_test.dart @@ -100,6 +100,11 @@ void main() { await pumpAndConnectDevTools(tester, testApp); await tester.pump(longDuration); + // TODO(kenz): re-work this integration test so that we do not have to be + // on the inspector screen for this to pass. + await switchToScreen(tester, ScreenMetaData.inspector); + await tester.pump(longDuration); + // Ensure all futures are completed before running checks. await serviceManager.service!.allFuturesCompleted; diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index 51a634b2f44..fbc0f30808e 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -6,7 +6,7 @@ export 'src/app.dart'; export 'src/extension_points/extensions_base.dart'; export 'src/extension_points/extensions_external.dart'; export 'src/framework/app_bar.dart'; -export 'src/framework/landing_screen.dart'; +export 'src/framework/home_screen.dart'; export 'src/framework/notifications_view.dart'; export 'src/framework/release_notes/release_notes.dart'; export 'src/framework/status_line.dart'; @@ -75,7 +75,6 @@ export 'src/shared/console/eval/eval_service.dart'; export 'src/shared/console/eval/inspector_tree.dart'; export 'src/shared/console/primitives/simple_items.dart'; export 'src/shared/console/widgets/description.dart'; -export 'src/shared/device_dialog.dart'; export 'src/shared/diagnostics/diagnostics_node.dart'; export 'src/shared/diagnostics/inspector_service.dart'; export 'src/shared/error_badge_manager.dart'; diff --git a/packages/devtools_app/lib/initialization.dart b/packages/devtools_app/lib/initialization.dart index fcf4392abeb..d835928c7c4 100644 --- a/packages/devtools_app/lib/initialization.dart +++ b/packages/devtools_app/lib/initialization.dart @@ -35,7 +35,7 @@ void runDevTools({ List? screens, }) { setupErrorHandling(() async { - screens ??= defaultScreens; + screens ??= defaultScreens(sampleData: sampleData); initDevToolsLogging(); diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 9d35533a55a..3e7ce70d78b 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -10,8 +10,8 @@ import 'package:provider/provider.dart'; import 'example/conditional_screen.dart'; import 'framework/framework_core.dart'; +import 'framework/home_screen.dart'; import 'framework/initializer.dart'; -import 'framework/landing_screen.dart'; import 'framework/notifications_view.dart'; import 'framework/release_notes/release_notes.dart'; import 'framework/scaffold.dart'; @@ -201,15 +201,7 @@ class DevToolsAppState extends State with AutoDisposeMixin { ) { final vmServiceUri = params['uri']; final embed = isEmbedded(params); - - // Always return the landing screen if there's no VM service URI. - if (vmServiceUri?.isEmpty ?? true) { - return DevToolsScaffold.withChild( - key: const Key('landing'), - embed: embed, - child: LandingScreenBody(sampleData: widget.sampleData), - ); - } + final hide = {...?params['hide']?.split(',')}; // TODO(dantup): We should be able simplify this a little, removing params['page'] // and only supporting /inspector (etc.) instead of also &page=inspector if @@ -217,49 +209,49 @@ class DevToolsAppState extends State with AutoDisposeMixin { if (page?.isEmpty ?? true) { page = params['page']; } - final hide = {...?params['hide']?.split(',')}; - return Initializer( - url: vmServiceUri, - allowConnectionScreenOnDisconnect: !embed, - builder: (_) { - // Force regeneration of visible screens when VM developer mode is - // enabled. - return ValueListenableBuilder( - valueListenable: preferences.vmDeveloperModeEnabled, - builder: (_, __, child) { - final screens = _visibleScreens() - .where((p) => embed && page != null ? p.screenId == page : true) - .where((p) => !hide.contains(p.screenId)) - .toList(); - if (screens.isEmpty) return child ?? const SizedBox.shrink(); - return MultiProvider( - providers: _providedControllers(), - child: DevToolsScaffold( - embed: embed, - page: page, - screens: screens, - actions: [ + + final screens = _visibleScreens() + .where((p) => embed && page != null ? p.screenId == page : true) + .where((p) => !hide.contains(p.screenId)) + .toList(); + + final connectedToVmService = + vmServiceUri != null && vmServiceUri.isNotEmpty; + + Widget scaffoldBuilder() { + // Force regeneration of visible screens when VM developer mode is + // enabled. + return ValueListenableBuilder( + valueListenable: preferences.vmDeveloperModeEnabled, + builder: (_, __, child) { + return MultiProvider( + providers: _providedControllers(), + child: DevToolsScaffold( + embed: embed, + page: page, + screens: screens, + actions: [ + if (connectedToVmService) // TODO(https://github.com/flutter/devtools/issues/1941) if (serviceManager.connectedApp!.isFlutterAppNow!) ...[ const HotReloadButton(), const HotRestartButton(), ], - ...DevToolsScaffold.defaultActions(), - ], - ), - ); - }, - child: DevToolsScaffold.withChild( - embed: embed, - child: CenteredMessage( - page != null - ? 'The "$page" screen is not available for this application.' - : 'No tabs available for this application.', + ...DevToolsScaffold.defaultActions(), + ], ), - ), - ); - }, - ); + ); + }, + ); + } + + return connectedToVmService + ? Initializer( + url: vmServiceUri, + allowConnectionScreenOnDisconnect: !embed, + builder: (_) => scaffoldBuilder(), + ) + : scaffoldBuilder(); } /// The pages that the app exposes. @@ -475,8 +467,14 @@ class _AlternateCheckedModeBanner extends StatelessWidget { /// /// Conditional screens can be added to this list, and they will automatically /// be shown or hidden based on the [Screen.conditionalLibrary] provided. -List get defaultScreens { +List defaultScreens({ + List sampleData = const [], +}) { return devtoolsScreens ??= [ + DevToolsScreen( + HomeScreen(sampleData: sampleData), + createController: (_) {}, + ), DevToolsScreen( InspectorScreen(), createController: (_) => InspectorController( diff --git a/packages/devtools_app/lib/src/framework/app_bar.dart b/packages/devtools_app/lib/src/framework/app_bar.dart index 9fad3167bad..307ff763f2f 100644 --- a/packages/devtools_app/lib/src/framework/app_bar.dart +++ b/packages/devtools_app/lib/src/framework/app_bar.dart @@ -15,15 +15,12 @@ class DevToolsAppBar extends StatelessWidget { const DevToolsAppBar({ super.key, required this.tabController, - required this.title, required this.screens, required this.actions, }); final TabController? tabController; - final String title; - final List screens; final List? actions; @@ -37,7 +34,6 @@ class DevToolsAppBar extends StatelessWidget { List visibleScreens = screens; bool tabsOverflow({bool includeOverflowButtonWidth = false}) { return _scaffoldHeaderWidth( - title: title, screens: visibleScreens, actions: actions, textTheme: textTheme, @@ -46,13 +42,11 @@ class DevToolsAppBar extends StatelessWidget { MediaQuery.of(context).size.width; } - var hideTitle = false; var overflow = tabsOverflow(); while (overflow) { visibleScreens = List.of(visibleScreens)..safeRemoveLast(); overflow = tabsOverflow(includeOverflowButtonWidth: true); if (overflow && visibleScreens.isEmpty) { - hideTitle = true; break; } } @@ -82,12 +76,6 @@ class DevToolsAppBar extends StatelessWidget { ], ); - final leftPadding = hideTitle - ? 0.0 - : calculateTitleWidth( - title, - textTheme: Theme.of(context).textTheme, - ); final rightPadding = math.max( 0.0, // Use [actions] here instead of [actionsWithSpacer] because we may @@ -104,7 +92,6 @@ class DevToolsAppBar extends StatelessWidget { child: Padding( padding: EdgeInsets.only( top: densePadding, - left: leftPadding, right: rightPadding, ), child: Row( @@ -135,7 +122,6 @@ class DevToolsAppBar extends StatelessWidget { return AppBar( // Turn off the appbar's back button. automaticallyImplyLeading: false, - title: hideTitle ? const SizedBox.shrink() : DevToolsTitle(title: title), centerTitle: false, toolbarHeight: defaultToolbarHeight, actions: actionsWithSpacer, @@ -145,18 +131,16 @@ class DevToolsAppBar extends StatelessWidget { /// Returns the width of the scaffold title, tabs and default icons. double _scaffoldHeaderWidth({ - required String title, required List screens, required List? actions, required TextTheme textTheme, }) { - final titleWidth = calculateTitleWidth(title, textTheme: textTheme); final tabsWidth = screens.fold( 0.0, (prev, screen) => prev + screen.approximateTabWidth(textTheme), ); final actionsWidth = (actions?.length ?? 0) * actionWidgetSize; - return titleWidth + tabsWidth + actionsWidth; + return tabsWidth + actionsWidth; } } @@ -267,46 +251,3 @@ class SelectedTabWrapper extends StatelessWidget { ); } } - -class DevToolsTitle extends StatelessWidget { - const DevToolsTitle({super.key, required this.title}); - - final String title; - - static double get paddingSize => - intermediateSpacing * 2 + VerticalLineSpacer.totalWidth; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - title, - style: Theme.of(context).devToolsTitleStyle, - ), - const SizedBox(width: intermediateSpacing), - VerticalLineSpacer(height: defaultToolbarHeight), - ], - ); - } -} - -// TODO(kenz): make private once app bar code is refactored out of scaffold.dart -// and into this file. -double calculateTitleWidth( - String title, { - required TextTheme textTheme, - bool includeTitlePadding = true, -}) { - final painter = TextPainter( - text: TextSpan( - text: title, - style: textTheme.titleMedium, - ), - textDirection: TextDirection.ltr, - )..layout(); - // Approximate size of the title. Add [defaultSpacing] to account for - // title's leading padding. - return painter.width + (includeTitlePadding ? DevToolsTitle.paddingSize : 0); -} diff --git a/packages/devtools_app/lib/src/framework/landing_screen.dart b/packages/devtools_app/lib/src/framework/home_screen.dart similarity index 78% rename from packages/devtools_app/lib/src/framework/landing_screen.dart rename to packages/devtools_app/lib/src/framework/home_screen.dart index 71902780628..d47c47e028b 100644 --- a/packages/devtools_app/lib/src/framework/landing_screen.dart +++ b/packages/devtools_app/lib/src/framework/home_screen.dart @@ -14,50 +14,75 @@ import '../shared/analytics/analytics.dart' as ga; import '../shared/analytics/constants.dart' as gac; import '../shared/common_widgets.dart'; import '../shared/config_specific/import_export/import_export.dart'; +import '../shared/connection_info.dart'; import '../shared/feature_flags.dart'; import '../shared/globals.dart'; +import '../shared/primitives/auto_dispose.dart'; import '../shared/primitives/blocking_action_mixin.dart'; import '../shared/primitives/utils.dart'; import '../shared/routing.dart'; +import '../shared/screen.dart'; import '../shared/theme.dart'; +import '../shared/title.dart'; import '../shared/ui/label.dart'; +import '../shared/ui/vm_flag_widgets.dart'; import '../shared/utils.dart'; import 'framework_core.dart'; -/// The landing screen when starting Dart DevTools without being connected to an -/// app. -/// -/// We need to use this screen to get a guarantee that the app has a Dart VM -/// available as well as to provide access to other functionality that does not -/// require a connected Dart application. -class LandingScreenBody extends StatefulWidget { - const LandingScreenBody({super.key, this.sampleData = const []}); +class HomeScreen extends Screen { + HomeScreen({this.sampleData = const []}) + : super.conditional( + id: id, + requiresConnection: false, + icon: ScreenMetaData.home.icon, + titleGenerator: () => devToolsTitle.value, + ); + + static final id = ScreenMetaData.home.id; + + final List sampleData; + + @override + Widget build(BuildContext context) { + return HomeScreenBody(sampleData: sampleData); + } +} + +class HomeScreenBody extends StatefulWidget { + const HomeScreenBody({super.key, this.sampleData = const []}); final List sampleData; @override - State createState() => _LandingScreenBodyState(); + State createState() => _HomeScreenBodyState(); } -class _LandingScreenBodyState extends State { +class _HomeScreenBodyState extends State with AutoDisposeMixin { @override void initState() { super.initState(); - ga.screen(gac.landingScreen); + ga.screen(gac.home); + + autoDisposeStreamSubscription( + serviceManager.onConnectionAvailable.listen((_) => setState(() {})), + ); } @override Widget build(BuildContext context) { + final connected = + serviceManager.hasConnection && serviceManager.connectedAppInitialized; return Scrollbar( child: ListView( children: [ - const ConnectDialog(), - const SizedBox(height: defaultSpacing), - if (widget.sampleData.isNotEmpty && !kReleaseMode) ...[ + ConnectionSection(connected: connected), + if (widget.sampleData.isNotEmpty && !kReleaseMode && !connected) ...[ SampleDataDropDownButton(sampleData: widget.sampleData), const SizedBox(height: defaultSpacing), ], - const AppSizeToolingInstructions(), + // TODO(polina-c): make the MemoryScreen a static screen and remove + // this section from the Home page. See this PR for more details: + // https://github.com/flutter/devtools/pull/6010. if (FeatureFlags.memoryAnalysis) ...[ const SizedBox(height: defaultSpacing), const MemoryAnalysisInstructions(), @@ -68,26 +93,69 @@ class _LandingScreenBodyState extends State { } } +class ConnectionSection extends StatelessWidget { + const ConnectionSection({super.key, required this.connected}); + + static const _primaryMinScreenWidthForTextBeforeScaling = 480.0; + static const _secondaryMinScreenWidthForTextBeforeScaling = 600.0; + + final bool connected; + + @override + Widget build(BuildContext context) { + if (connected) { + return LandingScreenSection( + title: 'Connected app', + actions: [ + ViewVmFlagsButton( + gaScreen: gac.home, + minScreenWidthForTextBeforeScaling: + _secondaryMinScreenWidthForTextBeforeScaling, + ), + const SizedBox(width: defaultSpacing), + ConnectToNewAppButton( + gaScreen: gac.home, + minScreenWidthForTextBeforeScaling: + _primaryMinScreenWidthForTextBeforeScaling, + ), + ], + child: const ConnectedAppSummary(narrowView: false), + ); + } + return const ConnectDialog(); + } +} + class LandingScreenSection extends StatelessWidget { const LandingScreenSection({ Key? key, required this.title, required this.child, + this.actions = const [], }) : super(key: key); final String title; final Widget child; + final List actions; + @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: textTheme.titleLarge, + Row( + children: [ + Expanded( + child: Text( + title, + style: textTheme.titleLarge, + ), + ), + ...actions, + ], ), const PaddedDivider(), child, @@ -212,7 +280,7 @@ class _ConnectDialogState extends State Future _connectHelper() async { ga.select( - gac.landingScreen, + gac.home, gac.HomeScreenEvents.connectToApp.name, ); if (connectDialogController.text.isEmpty) { @@ -260,46 +328,6 @@ class _ConnectDialogState extends State } } -class AppSizeToolingInstructions extends StatelessWidget { - const AppSizeToolingInstructions({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return LandingScreenSection( - title: 'App Size Tooling', - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Analyze and view diffs for your app\'s size', - style: textTheme.titleMedium, - ), - const SizedBox(height: denseRowSpacing), - Text( - 'Load Dart AOT snapshots or app size analysis files to ' - 'track down size issues in your app.', - style: textTheme.bodySmall, - ), - const SizedBox(height: defaultSpacing), - ElevatedButton( - child: const Text('Open app size tool'), - onPressed: () => _onOpenAppSizeToolSelected(context), - ), - ], - ), - ); - } - - void _onOpenAppSizeToolSelected(BuildContext context) { - ga.select( - gac.landingScreen, - gac.openAppSizeTool, - ); - DevToolsRouterDelegate.of(context).navigate(appSizeScreenId); - } -} - @visibleForTesting class MemoryAnalysisInstructions extends StatelessWidget { const MemoryAnalysisInstructions({Key? key}) : super(key: key); @@ -334,10 +362,6 @@ class MemoryAnalysisInstructions extends StatelessWidget { } void _onOpen(BuildContext context) { - ga.select( - gac.landingScreen, - gac.openMemoryAnalysisTool, - ); DevToolsRouterDelegate.of(context).navigate(memoryAnalysisScreenId); } } diff --git a/packages/devtools_app/lib/src/framework/scaffold.dart b/packages/devtools_app/lib/src/framework/scaffold.dart index 3438907ae64..e22cb24c527 100644 --- a/packages/devtools_app/lib/src/framework/scaffold.dart +++ b/packages/devtools_app/lib/src/framework/scaffold.dart @@ -16,7 +16,6 @@ import '../shared/console/widgets/console_pane.dart'; import '../shared/framework_controller.dart'; import '../shared/globals.dart'; import '../shared/primitives/auto_dispose.dart'; -import '../shared/primitives/simple_items.dart'; import '../shared/routing.dart'; import '../shared/screen.dart'; import '../shared/split.dart'; @@ -112,13 +111,12 @@ class DevToolsScaffoldState extends State late ImportController _importController; - late String scaffoldTitle; - @override void initState() { super.initState(); - _initTitle(); + addAutoDisposeListener(devToolsTitle); + _setupTabController(); addAutoDisposeListener(offlineController.offlineMode); @@ -168,15 +166,6 @@ class DevToolsScaffoldState extends State super.dispose(); } - void _initTitle() { - scaffoldTitle = devToolsTitle.value; - addAutoDisposeListener(devToolsTitle, () { - setState(() { - scaffoldTitle = devToolsTitle.value; - }); - }); - } - void _setupTabController() { _tabController?.dispose(); _tabController = TabController(length: widget.screens.length, vsync: this); @@ -313,66 +302,57 @@ class DevToolsScaffoldState extends State return DragAndDrop( handleDrop: _importController.importData, - child: Title( - title: scaffoldTitle, - // Color is a required parameter but the color only appears to - // matter on Android and we do not care about Android. - // Using theme.primaryColor matches the default behavior of the - // title used by [WidgetsApp]. - color: theme.primaryColor.withAlpha(255), - child: KeyboardShortcuts( - keyboardShortcuts: _currentScreen.buildKeyboardShortcuts( - context, - ), - child: Scaffold( - appBar: widget.embed - ? null - : PreferredSize( - preferredSize: Size.fromHeight(defaultToolbarHeight), - // Place the AppBar inside of a Hero widget to keep it the same across - // route transitions. - child: Hero( - tag: _appBarTag, - child: DevToolsAppBar( - tabController: _tabController, - title: scaffoldTitle, - screens: widget.screens, - actions: widget.actions, - ), + child: KeyboardShortcuts( + keyboardShortcuts: _currentScreen.buildKeyboardShortcuts( + context, + ), + child: Scaffold( + appBar: widget.embed + ? null + : PreferredSize( + preferredSize: Size.fromHeight(defaultToolbarHeight), + // Place the AppBar inside of a Hero widget to keep it the same across + // route transitions. + child: Hero( + tag: _appBarTag, + child: DevToolsAppBar( + tabController: _tabController, + screens: widget.screens, + actions: widget.actions, ), ), - body: OutlineDecoration.onlyTop( - child: Padding( - padding: widget.appPadding, - child: showConsole - ? Split( - axis: Axis.vertical, - splitters: [ - ConsolePaneHeader( - backgroundColor: theme.colorScheme.surface, - ), - ], - initialFractions: const [0.8, 0.2], - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: intermediateSpacing, - ), - child: content, + ), + body: OutlineDecoration.onlyTop( + child: Padding( + padding: widget.appPadding, + child: showConsole + ? Split( + axis: Axis.vertical, + splitters: [ + ConsolePaneHeader( + backgroundColor: theme.colorScheme.surface, + ), + ], + initialFractions: const [0.8, 0.2], + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: intermediateSpacing, ), - RoundedOutlinedBorder.onlyBottom( - child: const ConsolePane(), - ), - ], - ) - : content, - ), - ), - bottomNavigationBar: StatusLine( - currentScreen: _currentScreen, - isEmbedded: widget.embed, + child: content, + ), + RoundedOutlinedBorder.onlyBottom( + child: const ConsolePane(), + ), + ], + ) + : content, ), ), + bottomNavigationBar: StatusLine( + currentScreen: _currentScreen, + isEmbedded: widget.embed, + ), ), ), ); diff --git a/packages/devtools_app/lib/src/framework/status_line.dart b/packages/devtools_app/lib/src/framework/status_line.dart index f6c7e214fbd..10d4ad33f30 100644 --- a/packages/devtools_app/lib/src/framework/status_line.dart +++ b/packages/devtools_app/lib/src/framework/status_line.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:vm_service/vm_service.dart'; @@ -12,7 +10,6 @@ import '../service/isolate_manager.dart'; import '../service/service_manager.dart'; import '../shared/analytics/constants.dart' as gac; import '../shared/common_widgets.dart'; -import '../shared/device_dialog.dart'; import '../shared/globals.dart'; import '../shared/screen.dart'; import '../shared/theme.dart'; @@ -163,34 +160,11 @@ class StatusLine extends StatelessWidget { ), const SizedBox(width: denseSpacing), DevToolsTooltip( - message: deviceInfoTooltip, - child: InkWell( - onTap: () { - unawaited( - showDialog( - context: context, - builder: (context) => DeviceDialog( - connectedApp: app, - ), - ), - ); - }, - child: Row( - children: [ - Icon( - Icons.info_outline, - size: actionsIconSize, - ), - if (screenWidth > MediaSize.xxs) ...[ - const SizedBox(width: denseSpacing), - Text( - description, - style: textTheme.bodyMedium, - overflow: TextOverflow.clip, - ), - ], - ], - ), + message: 'Connected device', + child: Text( + description, + style: textTheme.bodyMedium, + overflow: TextOverflow.clip, ), ), ], diff --git a/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart b/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart index 41e8d3a679e..0b0d756569a 100644 --- a/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart +++ b/packages/devtools_app/lib/src/screens/app_size/app_size_screen.dart @@ -18,12 +18,10 @@ import '../../shared/config_specific/url/url.dart'; import '../../shared/file_import.dart'; import '../../shared/globals.dart'; import '../../shared/primitives/auto_dispose.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/screen.dart'; import '../../shared/split.dart'; import '../../shared/theme.dart'; -import '../../shared/ui/icons.dart'; import '../../shared/ui/tab.dart'; import '../../shared/utils.dart'; import 'app_size_controller.dart'; @@ -37,18 +35,15 @@ class AppSizeScreen extends Screen { AppSizeScreen() : super.conditional( id: id, + requiresConnection: false, requiresDartVm: true, title: ScreenMetaData.appSize.title, - icon: Octicons.fileZip, + icon: ScreenMetaData.appSize.icon, ); static const analysisTabKey = Key('Analysis Tab'); static const diffTabKey = Key('Diff Tab'); - /// The ID (used in routing) for the tabbed app-size page. - /// - /// This must be different to the top-level appSizePageId which is also used - /// in routing when to ensure they have unique URLs. static final id = ScreenMetaData.appSize.id; @visibleForTesting diff --git a/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart b/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart index a4f71eebaef..d89638b7075 100644 --- a/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart +++ b/packages/devtools_app/lib/src/screens/debugger/debugger_screen.dart @@ -19,12 +19,10 @@ import '../../shared/flex_split_column.dart'; import '../../shared/globals.dart'; import '../../shared/primitives/auto_dispose.dart'; import '../../shared/primitives/listenable.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/routing.dart'; import '../../shared/screen.dart'; import '../../shared/split.dart'; import '../../shared/theme.dart'; -import '../../shared/ui/icons.dart'; import '../../shared/utils.dart'; import 'breakpoints.dart'; import 'call_stack.dart'; @@ -44,7 +42,7 @@ class DebuggerScreen extends Screen { id: id, requiresDebugBuild: true, title: ScreenMetaData.debugger.title, - icon: Octicons.bug, + icon: ScreenMetaData.debugger.icon, showFloatingDebuggerControls: false, ); diff --git a/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart b/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart index 81765f3f6a7..766347b6609 100644 --- a/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart +++ b/packages/devtools_app/lib/src/screens/inspector/inspector_screen.dart @@ -21,11 +21,9 @@ import '../../shared/error_badge_manager.dart'; import '../../shared/globals.dart'; import '../../shared/primitives/auto_dispose.dart'; import '../../shared/primitives/blocking_action_mixin.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/split.dart'; import '../../shared/theme.dart'; -import '../../shared/ui/icons.dart'; import '../../shared/ui/search.dart'; import '../../shared/utils.dart'; import 'inspector_controller.dart'; @@ -39,7 +37,7 @@ class InspectorScreen extends Screen { requiresLibrary: flutterLibraryUri, requiresDebugBuild: true, title: ScreenMetaData.inspector.title, - icon: Octicons.deviceMobile, + icon: ScreenMetaData.inspector.icon, ); static final id = ScreenMetaData.inspector.id; diff --git a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart index f69ebae25bc..966f8e7bcd8 100644 --- a/packages/devtools_app/lib/src/screens/logging/logging_screen.dart +++ b/packages/devtools_app/lib/src/screens/logging/logging_screen.dart @@ -12,12 +12,10 @@ import '../../shared/analytics/analytics.dart' as ga; import '../../shared/analytics/constants.dart' as gac; import '../../shared/common_widgets.dart'; import '../../shared/primitives/auto_dispose.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/split.dart'; import '../../shared/theme.dart'; import '../../shared/ui/filter.dart'; -import '../../shared/ui/icons.dart'; import '../../shared/ui/search.dart'; import '../../shared/utils.dart'; import '_log_details.dart'; @@ -30,7 +28,7 @@ class LoggingScreen extends Screen { : super( id, title: ScreenMetaData.logging.title, - icon: Octicons.clippy, + icon: ScreenMetaData.logging.icon, ); static final id = ScreenMetaData.logging.id; diff --git a/packages/devtools_app/lib/src/screens/memory/framework/connected/connected_screen_body.dart b/packages/devtools_app/lib/src/screens/memory/framework/connected/connected_screen_body.dart index 068e887b04c..5c88bd95bd2 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/connected/connected_screen_body.dart +++ b/packages/devtools_app/lib/src/screens/memory/framework/connected/connected_screen_body.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import '../../../../shared/banner_messages.dart'; import '../../../../shared/http/http_service.dart' as http_service; import '../../../../shared/primitives/auto_dispose.dart'; -import '../../../../shared/primitives/simple_items.dart'; +import '../../../../shared/screen.dart'; import '../../../../shared/theme.dart'; import '../../../../shared/utils.dart'; import '../../panes/chart/chart_pane.dart'; diff --git a/packages/devtools_app/lib/src/screens/memory/framework/memory_screen.dart b/packages/devtools_app/lib/src/screens/memory/framework/memory_screen.dart index 1a5a6566ad9..f1e864b99a1 100644 --- a/packages/devtools_app/lib/src/screens/memory/framework/memory_screen.dart +++ b/packages/devtools_app/lib/src/screens/memory/framework/memory_screen.dart @@ -7,9 +7,7 @@ import 'package:flutter/material.dart'; import '../../../shared/analytics/analytics.dart' as ga; import '../../../shared/primitives/listenable.dart'; -import '../../../shared/primitives/simple_items.dart'; import '../../../shared/screen.dart'; -import '../../../shared/ui/icons.dart'; import 'connected/connected_screen_body.dart'; class MemoryScreen extends Screen { @@ -18,7 +16,7 @@ class MemoryScreen extends Screen { id: id, requiresDartVm: true, title: ScreenMetaData.memory.title, - icon: Octicons.package, + icon: ScreenMetaData.memory.icon, ); static final id = ScreenMetaData.memory.id; diff --git a/packages/devtools_app/lib/src/screens/network/network_screen.dart b/packages/devtools_app/lib/src/screens/network/network_screen.dart index 4cc6dc15353..c3f9108f868 100644 --- a/packages/devtools_app/lib/src/screens/network/network_screen.dart +++ b/packages/devtools_app/lib/src/screens/network/network_screen.dart @@ -15,7 +15,6 @@ import '../../shared/globals.dart'; import '../../shared/http/curl_command.dart'; import '../../shared/http/http_request_data.dart'; import '../../shared/primitives/auto_dispose.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/primitives/utils.dart'; import '../../shared/screen.dart'; import '../../shared/split.dart'; @@ -35,7 +34,7 @@ class NetworkScreen extends Screen { id: id, requiresDartVm: true, title: ScreenMetaData.network.title, - icon: Icons.network_check, + icon: ScreenMetaData.network.icon, ); static final id = ScreenMetaData.network.id; 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 387ba678138..faa2b24f7b1 100644 --- a/packages/devtools_app/lib/src/screens/performance/performance_screen.dart +++ b/packages/devtools_app/lib/src/screens/performance/performance_screen.dart @@ -10,10 +10,8 @@ import '../../shared/banner_messages.dart'; import '../../shared/common_widgets.dart'; import '../../shared/globals.dart'; import '../../shared/primitives/auto_dispose.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/theme.dart'; -import '../../shared/ui/icons.dart'; import '../../shared/utils.dart'; import 'panes/controls/performance_controls.dart'; import 'panes/flutter_frames/flutter_frames_chart.dart'; @@ -30,7 +28,7 @@ class PerformanceScreen extends Screen { requiresDartVm: true, worksOffline: true, title: ScreenMetaData.performance.title, - icon: Octicons.pulse, + icon: ScreenMetaData.performance.icon, ); static final id = ScreenMetaData.performance.id; 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 2e194a3b9b7..03ccd49869d 100644 --- a/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart +++ b/packages/devtools_app/lib/src/screens/profiler/profiler_screen.dart @@ -12,10 +12,8 @@ import '../../shared/common_widgets.dart'; import '../../shared/globals.dart'; import '../../shared/primitives/auto_dispose.dart'; import '../../shared/primitives/listenable.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/theme.dart'; -import '../../shared/ui/icons.dart'; import '../../shared/utils.dart'; import 'cpu_profile_model.dart'; import 'cpu_profiler.dart'; @@ -31,7 +29,7 @@ class ProfilerScreen extends Screen { requiresDartVm: true, worksOffline: true, title: ScreenMetaData.cpuProfiler.title, - icon: Octicons.dashboard, + icon: ScreenMetaData.cpuProfiler.icon, ); static final id = ScreenMetaData.cpuProfiler.id; diff --git a/packages/devtools_app/lib/src/screens/provider/provider_screen.dart b/packages/devtools_app/lib/src/screens/provider/provider_screen.dart index 21751c7d397..f97247bfbfb 100644 --- a/packages/devtools_app/lib/src/screens/provider/provider_screen.dart +++ b/packages/devtools_app/lib/src/screens/provider/provider_screen.dart @@ -13,7 +13,6 @@ import '../../shared/banner_messages.dart'; import '../../shared/common_widgets.dart'; import '../../shared/dialogs.dart'; import '../../shared/globals.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/split.dart'; import 'instance_viewer/instance_details.dart'; @@ -52,8 +51,8 @@ class ProviderScreen extends Screen { id: id, requiresLibrary: 'package:provider/', title: ScreenMetaData.provider.title, + icon: ScreenMetaData.provider.icon, requiresDebugBuild: true, - icon: Icons.attach_file, ); static final id = ScreenMetaData.provider.id; diff --git a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart index 2390873d022..29d587982b4 100644 --- a/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart +++ b/packages/devtools_app/lib/src/screens/vm_developer/vm_developer_tools_screen.dart @@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../../shared/primitives/auto_dispose.dart'; -import '../../shared/primitives/simple_items.dart'; import '../../shared/screen.dart'; import '../../shared/theme.dart'; import '../../shared/utils.dart'; @@ -44,7 +43,7 @@ class VMDeveloperToolsScreen extends Screen { : super.conditional( id: id, title: ScreenMetaData.vmTools.title, - icon: Icons.settings_applications, + icon: ScreenMetaData.vmTools.icon, requiresVmDeveloperMode: true, ); diff --git a/packages/devtools_app/lib/src/shared/analytics/constants.dart b/packages/devtools_app/lib/src/shared/analytics/constants.dart index fe03e1c850e..a29a0080592 100644 --- a/packages/devtools_app/lib/src/shared/analytics/constants.dart +++ b/packages/devtools_app/lib/src/shared/analytics/constants.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. -import '../primitives/simple_items.dart'; +import '../screen.dart'; // Type of events (event_category): const screenViewEvent = 'screen'; // Active screen (tab selected). @@ -13,6 +13,7 @@ const timingEvent = 'timing'; // Timed operation. // These screen ids must match the `screenId` for each respective subclass of // [Screen]. This is to ensure that the analytics for documentation links match // the screen id for other analytics on the same screen. +final home = ScreenMetaData.home.id; final inspector = ScreenMetaData.inspector.id; final performance = ScreenMetaData.performance.id; final cpuProfiler = ScreenMetaData.cpuProfiler.id; @@ -149,11 +150,6 @@ const refreshIsolateStatistics = 'refreshIsolateStatistics'; const refreshVmStatistics = 'refreshVmStatistics'; const requestSize = 'requestSize'; -// Landing screen UX actions: -const landingScreen = 'landing'; -const openAppSizeTool = 'openAppSizeTool'; -const openMemoryAnalysisTool = 'openMemoryAnalysisTool'; - // Settings actions: const settingsDialog = 'settings'; const darkTheme = 'darkTheme'; 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 19a272bca3c..bb8d658d9da 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 @@ -18,6 +18,7 @@ import '../../file_import.dart'; import '../../globals.dart'; import '../../primitives/simple_items.dart'; import '../../primitives/utils.dart'; +import '../../screen.dart'; import '../../theme.dart'; import '_export_stub.dart' if (dart.library.html) '_export_web.dart' diff --git a/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart b/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart index 0e8c59016d1..db35f1ec78f 100644 --- a/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart +++ b/packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart @@ -5,14 +5,14 @@ import 'package:flutter/material.dart' hide Stack; import 'package:vm_service/vm_service.dart'; -import '../../../shared/common_widgets.dart'; -import '../../../shared/globals.dart'; -import '../../../shared/primitives/selection_controls.dart'; -import '../../../shared/primitives/utils.dart'; -import '../../../shared/routing.dart'; -import '../../../shared/theme.dart'; +import '../../common_widgets.dart'; import '../../diagnostics/dart_object_node.dart'; -import '../../primitives/simple_items.dart'; +import '../../globals.dart'; +import '../../primitives/selection_controls.dart'; +import '../../primitives/utils.dart'; +import '../../routing.dart'; +import '../../screen.dart'; +import '../../theme.dart'; import 'description.dart'; VariableSelectionControls _selectionControls({ diff --git a/packages/devtools_app/lib/src/shared/device_dialog.dart b/packages/devtools_app/lib/src/shared/device_dialog.dart deleted file mode 100644 index 797d78b6a9a..00000000000 --- a/packages/devtools_app/lib/src/shared/device_dialog.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:vm_service/vm_service.dart'; - -import 'analytics/constants.dart' as gac; -import 'connected_app.dart'; -import 'connection_info.dart'; -import 'dialogs.dart'; -import 'globals.dart'; -import 'ui/vm_flag_widgets.dart'; - -class DeviceDialog extends StatelessWidget { - const DeviceDialog({super.key, required this.connectedApp}); - - final ConnectedApp connectedApp; - - @override - Widget build(BuildContext context) { - final VM? vm = serviceManager.vm; - if (vm == null || !serviceManager.connectedAppInitialized) { - return const SizedBox(); - } - - return DevToolsDialog( - title: const DialogTitleText('Device Info'), - content: const ConnectedAppSummary(), - actions: [ - const ConnectToNewAppButton( - gaScreen: gac.devToolsMain, - elevated: true, - ), - if (connectedApp.isRunningOnDartVM!) - const ViewVmFlagsButton( - gaScreen: gac.devToolsMain, - elevated: true, - ), - const DialogCloseButton(), - ], - actionsAlignment: MainAxisAlignment.spaceBetween, - ); - } -} diff --git a/packages/devtools_app/lib/src/shared/primitives/simple_items.dart b/packages/devtools_app/lib/src/shared/primitives/simple_items.dart index 0c1fe2429b7..6ff6a6cfff7 100644 --- a/packages/devtools_app/lib/src/shared/primitives/simple_items.dart +++ b/packages/devtools_app/lib/src/shared/primitives/simple_items.dart @@ -25,26 +25,6 @@ class PackagePrefixes { static const dartUi = 'dart:ui'; } -enum ScreenMetaData { - inspector('inspector', 'Flutter Inspector'), - performance('performance', 'Performance'), - cpuProfiler('cpu-profiler', 'CPU Profiler'), - memory('memory', 'Memory'), - debugger('debugger', 'Debugger'), - network('network', 'Network'), - logging('logging', 'Logging'), - provider('provider', 'Provider'), - appSize('app-size', 'App Size'), - vmTools('vm-tools', 'VM Tools'), - simple('simple', ''); - - const ScreenMetaData(this.id, this.title); - - final String id; - - final String title; -} - const String traceEventsFieldName = 'traceEvents'; const closureName = ''; diff --git a/packages/devtools_app/lib/src/shared/screen.dart b/packages/devtools_app/lib/src/shared/screen.dart index bf720efbf25..7248e4e65a8 100644 --- a/packages/devtools_app/lib/src/shared/screen.dart +++ b/packages/devtools_app/lib/src/shared/screen.dart @@ -11,42 +11,75 @@ import 'package:logging/logging.dart'; import 'globals.dart'; import 'primitives/listenable.dart'; import 'theme.dart'; +import 'ui/icons.dart'; import 'version.dart'; final _log = Logger('screen.dart'); +enum ScreenMetaData { + home('home', icon: Icons.home_rounded), + inspector( + 'inspector', + title: 'Flutter Inspector', + icon: Octicons.deviceMobile, + ), + performance('performance', title: 'Performance', icon: Octicons.pulse), + cpuProfiler('cpu-profiler', title: 'CPU Profiler', icon: Octicons.dashboard), + memory('memory', title: 'Memory', icon: Octicons.package), + debugger('debugger', title: 'Debugger', icon: Octicons.bug), + network('network', title: 'Network', icon: Icons.network_check), + logging('logging', title: 'Logging', icon: Octicons.clippy), + provider('provider', title: 'Provider', icon: Icons.attach_file), + appSize('app-size', title: 'App Size', icon: Octicons.fileZip), + vmTools('vm-tools', title: 'VM Tools', icon: Icons.settings_applications), + simple('simple'); + + const ScreenMetaData(this.id, {this.title, this.icon}); + + final String id; + + final String? title; + + final IconData? icon; +} + /// Defines a page shown in the DevTools [TabBar]. @immutable abstract class Screen { const Screen( this.screenId, { - this.title = '', + this.title, + this.titleGenerator, this.icon, this.tabKey, this.requiresLibrary, + this.requiresConnection = true, this.requiresDartVm = false, this.requiresDebugBuild = false, this.requiresVmDeveloperMode = false, this.worksOffline = false, this.shouldShowForFlutterVersion, this.showFloatingDebuggerControls = true, - }); + }) : assert((title == null) || (titleGenerator == null)); const Screen.conditional({ required String id, String? requiresLibrary, + bool requiresConnection = true, bool requiresDartVm = false, bool requiresDebugBuild = false, bool requiresVmDeveloperMode = false, bool worksOffline = false, bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion, bool showFloatingDebuggerControls = true, - String title = '', + String? title, + String Function()? titleGenerator, IconData? icon, Key? tabKey, }) : this( id, requiresLibrary: requiresLibrary, + requiresConnection: requiresConnection, requiresDartVm: requiresDartVm, requiresDebugBuild: requiresDebugBuild, requiresVmDeveloperMode: requiresVmDeveloperMode, @@ -54,6 +87,7 @@ abstract class Screen { shouldShowForFlutterVersion: shouldShowForFlutterVersion, showFloatingDebuggerControls: showFloatingDebuggerControls, title: title, + titleGenerator: titleGenerator, icon: icon, tabKey: tabKey, ); @@ -74,7 +108,16 @@ abstract class Screen { final String screenId; /// The user-facing name of the page. - final String title; + /// + /// At most, only one of [title] and [titleGenerator] should be non-null. + final String? title; + + /// A callback that returns the user-facing name of the page. + /// + /// At most, only one of [title] and [titleGenerator] should be non-null. + final String Function()? titleGenerator; + + String get _userFacingTitle => title ?? titleGenerator?.call() ?? ''; final IconData? icon; @@ -92,6 +135,9 @@ abstract class Screen { /// * 'package:provider/' final String? requiresLibrary; + /// Whether this screen requires a running app connection to work. + final bool requiresConnection; + /// Whether this screen should only be included when the app is running on the Dart VM. final bool requiresDartVm; @@ -129,6 +175,7 @@ abstract class Screen { TextTheme textTheme, { bool includeTabBarSpacing = true, }) { + final title = _userFacingTitle; final painter = TextPainter( text: TextSpan(text: title), textDirection: TextDirection.ltr, @@ -145,6 +192,7 @@ abstract class Screen { /// This will not be used if the [Screen] is the only one shown in the /// scaffold. Widget buildTab(BuildContext context) { + final title = _userFacingTitle; return ValueListenableBuilder( valueListenable: serviceManager.errorBadgeManager.errorCountNotifier(screenId), @@ -154,10 +202,11 @@ abstract class Screen { child: Row( children: [ Icon(icon, size: defaultIconSize), - Padding( - padding: const EdgeInsets.only(left: denseSpacing), - child: Text(title), - ), + if (title.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: denseSpacing), + child: Text(title), + ), ], ), ); @@ -215,14 +264,20 @@ bool shouldShowScreen(Screen screen) { _log.finest('for offline mode: returning ${screen.worksOffline}'); return screen.worksOffline; } - // No sense in ever showing screens in non-offline mode unless the service - // is available. This also avoids odd edge cases where we could show screens - // while the ServiceManager is still initializing. + final serviceReady = serviceManager.isServiceAvailable && serviceManager.connectedApp!.connectedAppInitialized; if (!serviceReady) { - _log.finest('service not ready: returning false'); - return false; + if (!screen.requiresConnection) { + _log.finest('screen does not require connection: returning true'); + return true; + } else { + // All of the following checks require a connected vm service, so verify + // that one exists. This also avoids odd edge cases where we could show + // screens while the ServiceManager is still initializing. + _log.finest('service not ready: returning false'); + return false; + } } if (screen.requiresLibrary != null) { diff --git a/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart b/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart index 6badb0fcff9..a92f79e5e24 100644 --- a/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart +++ b/packages/devtools_app/lib/src/shared/ui/vm_flag_widgets.dart @@ -94,7 +94,7 @@ class CpuSamplingRateDropdown extends StatelessWidget { value: samplingRate.value, child: Text(samplingRate.display), ), - gaId: samplingRate.displayShort + gaId: samplingRate.displayShort, ); } diff --git a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md index 9cf2a31d0df..d3b7608a9d9 100644 --- a/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md +++ b/packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md @@ -5,9 +5,15 @@ This is draft for future release notes, that are going to land on Dart & Flutter DevTools - A Suite of Performance Tools for Dart and Flutter -## General updates -* Fix overlay notifications so they cover the area that their background blocks - [#5975](https://github.com/flutter/devtools/pull/5975) +## General updates +* Added a new "Home" screen in DevTools that either shows the "Connect" dialog or +a summary of your connected app, depending on the connection status in DevTools. Keep an +eye on this screen for cool new features in the future. This change also enables support +for static tooling (tools that don't require a connected app) in DevTools - [#6010](https://github.com/flutter/devtools/pull/6010) +![home screen](images/home_screen.png "DevTools home screen") * Added an action to the main toolbar for loading offline data into DevTools - [#6003](https://github.com/flutter/devtools/pull/6003) +![load data action](images/load_data.png "Load data action") +* Fixed overlay notifications so that they cover the area that their background blocks - [#5975](https://github.com/flutter/devtools/pull/5975) ## Inspector updates TODO: Remove this section if there are not any general updates. diff --git a/packages/devtools_app/release_notes/images/home_screen.png b/packages/devtools_app/release_notes/images/home_screen.png new file mode 100644 index 00000000000..55d197ee381 Binary files /dev/null and b/packages/devtools_app/release_notes/images/home_screen.png differ diff --git a/packages/devtools_app/release_notes/images/load_data.png b/packages/devtools_app/release_notes/images/load_data.png new file mode 100644 index 00000000000..857b9b92125 Binary files /dev/null and b/packages/devtools_app/release_notes/images/load_data.png differ diff --git a/packages/devtools_app/test/shared/device_dialog_test.dart b/packages/devtools_app/test/shared/connection_info_test.dart similarity index 76% rename from packages/devtools_app/test/shared/device_dialog_test.dart rename to packages/devtools_app/test/shared/connection_info_test.dart index e19dcc0ced9..5c9e2fdab70 100644 --- a/packages/devtools_app/test/shared/device_dialog_test.dart +++ b/packages/devtools_app/test/shared/connection_info_test.dart @@ -1,11 +1,10 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/service/service_registrations.dart' as registrations; -import 'package:devtools_app/src/shared/ui/vm_flag_widgets.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -16,7 +15,7 @@ void main() { const windowSize = Size(2000.0, 1000.0); - group('DeviceDialog', () { + group('Connection info', () { void initServiceManager({ bool flutterVersionServiceAvailable = true, }) { @@ -42,14 +41,12 @@ void main() { setGlobal(IdeTheme, IdeTheme()); } - DeviceDialog deviceDialog; - setUp(() { initServiceManager(); }); testWidgetsWithWindowSize( - 'builds dialog for dart web app', + 'builds summary for dart web app', windowSize, (WidgetTester tester) async { final app = fakeServiceManager.connectedApp!; @@ -61,13 +58,7 @@ void main() { isWebApp: true, ); - deviceDialog = DeviceDialog( - connectedApp: app, - ); - - await tester.pumpWidget(wrap(deviceDialog)); - expect(find.text('Device Info'), findsOneWidget); - + await tester.pumpWidget(wrap(const ConnectedAppSummary())); expect(find.text('CPU / OS: '), findsOneWidget); expect(find.text('Web macos'), findsOneWidget); expect(find.text('Dart Version: '), findsOneWidget); @@ -98,13 +89,7 @@ void main() { isWebApp: false, ); - deviceDialog = DeviceDialog( - connectedApp: app, - ); - - await tester.pumpWidget(wrap(deviceDialog)); - expect(find.text('Device Info'), findsOneWidget); - + await tester.pumpWidget(wrap(const ConnectedAppSummary())); expect(find.text('CPU / OS: '), findsOneWidget); expect(find.text('x64 (64 bit) macos'), findsOneWidget); expect(find.text('Dart Version: '), findsOneWidget); @@ -134,13 +119,7 @@ void main() { isWebApp: false, ); - deviceDialog = DeviceDialog( - connectedApp: app, - ); - - await tester.pumpWidget(wrap(deviceDialog)); - expect(find.text('Device Info'), findsOneWidget); - + await tester.pumpWidget(wrap(const ConnectedAppSummary())); expect(find.text('CPU / OS: '), findsOneWidget); expect(find.text('x64 (64 bit) android'), findsOneWidget); expect(find.text('Dart Version: '), findsOneWidget); @@ -172,13 +151,7 @@ void main() { isWebApp: false, ); - deviceDialog = DeviceDialog( - connectedApp: app, - ); - - await tester.pumpWidget(wrap(deviceDialog)); - expect(find.text('Device Info'), findsOneWidget); - + await tester.pumpWidget(wrap(const ConnectedAppSummary())); expect(find.text('CPU / OS: '), findsOneWidget); expect(find.text('Dart Version: '), findsOneWidget); expect(find.text('1.9.1'), findsOneWidget); @@ -210,13 +183,7 @@ void main() { isWebApp: true, ); - deviceDialog = DeviceDialog( - connectedApp: app, - ); - - await tester.pumpWidget(wrap(deviceDialog)); - expect(find.text('Device Info'), findsOneWidget); - + await tester.pumpWidget(wrap(const ConnectedAppSummary())); expect(find.text('CPU / OS: '), findsOneWidget); expect(find.text('Web macos'), findsOneWidget); expect(find.text('Dart Version: '), findsOneWidget); @@ -249,13 +216,7 @@ void main() { isWebApp: true, ); - deviceDialog = DeviceDialog( - connectedApp: app, - ); - - await tester.pumpWidget(wrap(deviceDialog)); - expect(find.text('Device Info'), findsOneWidget); - + await tester.pumpWidget(wrap(const ConnectedAppSummary())); expect(find.text('CPU / OS: '), findsOneWidget); expect(find.text('Web macos'), findsOneWidget); expect(find.text('Dart Version: '), findsOneWidget); @@ -275,48 +236,4 @@ void main() { }, ); }); - - group('VMFlagsDialog', () { - void initServiceManager({ - bool flutterVersionServiceAvailable = true, - }) { - final availableServices = [ - if (flutterVersionServiceAvailable) - registrations.flutterVersion.service, - ]; - fakeServiceManager = FakeServiceManager( - availableServices: availableServices, - ); - when(fakeServiceManager.vm.version).thenReturn('1.9.1'); - final app = fakeServiceManager.connectedApp!; - when(app.isDartWebAppNow).thenReturn(false); - when(app.isRunningOnDartVM).thenReturn(true); - setGlobal(ServiceConnectionManager, fakeServiceManager); - } - - late VMFlagsDialog vmFlagsDialog; - - setUp(() { - initServiceManager(); - - vmFlagsDialog = const VMFlagsDialog(); - }); - - testWidgets('builds dialog', (WidgetTester tester) async { - mockConnectedApp( - fakeServiceManager.connectedApp!, - isFlutterApp: true, - isProfileBuild: false, - isWebApp: false, - ); - - await tester.pumpWidget(wrap(vmFlagsDialog)); - expect(find.richText('VM Flags'), findsOneWidget); - expect(find.richText('flag 1 name'), findsOneWidget); - final RichText commentText = tester.firstWidget( - findSubstring('flag 1 comment'), - ); - expect(commentText, isNotNull); - }); - }); } diff --git a/packages/devtools_app/test/shared/home_screen_test.dart b/packages/devtools_app/test/shared/home_screen_test.dart new file mode 100644 index 00000000000..80db2dae9c0 --- /dev/null +++ b/packages/devtools_app/test/shared/home_screen_test.dart @@ -0,0 +1,127 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/shared/ui/vm_flag_widgets.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + late FakeServiceManager fakeServiceManager; + + group('home screen with no app connection', () { + setUp(() { + setGlobal( + ServiceConnectionManager, + fakeServiceManager = FakeServiceManager(), + ); + setGlobal(IdeTheme, IdeTheme()); + fakeServiceManager.hasConnection = false; + }); + + testWidgetsWithWindowSize( + 'displays without error', + const Size(2000.0, 2000.0), + (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(wrap(const HomeScreenBody())); + expect(find.byType(ConnectionSection), findsOneWidget); + expect(find.byType(ConnectDialog), findsOneWidget); + expect(find.byType(ConnectToNewAppButton), findsNothing); + expect(find.byType(ViewVmFlagsButton), findsNothing); + expect(find.byType(SampleDataDropDownButton), findsNothing); + }, + ); + + testWidgetsWithWindowSize( + 'displays sample data picker as expected', + const Size(2000.0, 2000.0), + (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget( + wrap( + HomeScreenBody( + sampleData: [ + DevToolsJsonFile( + name: 'test-data', + lastModifiedTime: DateTime.now(), + data: {}, + ), + ], + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(SampleDataDropDownButton), findsOneWidget); + + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + + expect(find.text('test-data'), findsOneWidget); + }, + ); + }); + + group('home screen with app connection', () { + void initServiceManager() { + fakeServiceManager = FakeServiceManager(); + when(fakeServiceManager.vm.version).thenReturn('1.9.1'); + when(fakeServiceManager.vm.targetCPU).thenReturn('x64'); + when(fakeServiceManager.vm.architectureBits).thenReturn(64); + when(fakeServiceManager.vm.operatingSystem).thenReturn('android'); + final app = fakeServiceManager.connectedApp!; + mockConnectedApp( + app, + isFlutterApp: true, + isProfileBuild: false, + isWebApp: false, + ); + setGlobal(ServiceConnectionManager, fakeServiceManager); + setGlobal(IdeTheme, IdeTheme()); + } + + setUp(() { + initServiceManager(); + }); + + testWidgetsWithWindowSize( + 'displays without error', + const Size(2000.0, 2000.0), + (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(wrap(const HomeScreenBody())); + expect(find.byType(ConnectionSection), findsOneWidget); + expect(find.byType(ConnectDialog), findsNothing); + expect(find.byType(ConnectToNewAppButton), findsOneWidget); + expect(find.byType(ViewVmFlagsButton), findsOneWidget); + expect(find.byType(SampleDataDropDownButton), findsNothing); + }, + ); + + testWidgetsWithWindowSize( + 'does not display sample data picker', + const Size(2000.0, 2000.0), + (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget( + wrap( + HomeScreenBody( + sampleData: [ + DevToolsJsonFile( + name: 'test-data', + lastModifiedTime: DateTime.now(), + data: {}, + ), + ], + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.byType(SampleDataDropDownButton), findsNothing); + }, + ); + }); +} diff --git a/packages/devtools_app/test/shared/landing_screen_test.dart b/packages/devtools_app/test/shared/landing_screen_test.dart deleted file mode 100644 index 57346b73107..00000000000 --- a/packages/devtools_app/test/shared/landing_screen_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:devtools_app/devtools_app.dart'; -import 'package:devtools_test/devtools_test.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - setUp(() { - setGlobal(ServiceConnectionManager, FakeServiceManager()); - setGlobal(IdeTheme, IdeTheme()); - }); - - testWidgetsWithWindowSize( - 'Landing screen displays without error', - const Size(2000.0, 2000.0), - (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(wrap(const LandingScreenBody())); - expect(find.byType(ConnectDialog), findsOneWidget); - expect(find.byType(SampleDataDropDownButton), findsNothing); - expect(find.byType(AppSizeToolingInstructions), findsOneWidget); - }, - ); - - testWidgetsWithWindowSize( - 'Landing screen displays sample data picker', - const Size(2000.0, 2000.0), - (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget( - wrap( - LandingScreenBody( - sampleData: [ - DevToolsJsonFile( - name: 'test-data', - lastModifiedTime: DateTime.now(), - data: {}, - ), - ], - ), - ), - ); - expect(find.byType(ConnectDialog), findsOneWidget); - expect(find.byType(SampleDataDropDownButton), findsOneWidget); - expect(find.byType(AppSizeToolingInstructions), findsOneWidget); - - await tester.tap(find.byType(DropdownButton)); - await tester.pumpAndSettle(); - - expect(find.text('test-data'), findsOneWidget); - }, - ); -} diff --git a/packages/devtools_app/test/shared/scaffold_test.dart b/packages/devtools_app/test/shared/scaffold_test.dart index 0edcd77c652..4338073d428 100644 --- a/packages/devtools_app/test/shared/scaffold_test.dart +++ b/packages/devtools_app/test/shared/scaffold_test.dart @@ -60,7 +60,6 @@ void main() { expect(find.byKey(_t5), findsOneWidget); expect(find.byType(TabOverflowButton), findsNothing); - expect(find.byType(DevToolsTitle), findsOneWidget); }, ); @@ -84,7 +83,6 @@ void main() { expect(find.byKey(_t5), findsNothing); expect(find.byType(TabOverflowButton), findsOneWidget); - expect(find.byType(DevToolsTitle), findsOneWidget); }, ); @@ -107,7 +105,6 @@ void main() { expect(find.byKey(_t4), findsNothing); expect(find.byKey(_t5), findsNothing); expect(find.byType(TabOverflowButton), findsOneWidget); - expect(find.byType(DevToolsTitle), findsOneWidget); await tester.tap(find.byType(TabOverflowButton)); await tester.pumpAndSettle(); @@ -141,29 +138,6 @@ void main() { }, ); - testWidgetsWithWindowSize( - 'hides $DevToolsTitle when screen is very narrow', - const Size(220.0, 1200.0), - (WidgetTester tester) async { - await tester.pumpWidget( - wrapScaffold( - DevToolsScaffold( - screens: const [_screen1, _screen2, _screen3, _screen4, _screen5], - ), - ), - ); - expect(find.byKey(_k1), findsOneWidget); - - expect(find.byKey(_t1), findsNothing); - expect(find.byKey(_t2), findsNothing); - expect(find.byKey(_t3), findsNothing); - expect(find.byKey(_t4), findsNothing); - expect(find.byKey(_t5), findsNothing); - expect(find.byType(TabOverflowButton), findsOneWidget); - expect(find.byType(DevToolsTitle), findsNothing); - }, - ); - testWidgets( 'displays no tabs when only one is given', (WidgetTester tester) async { diff --git a/packages/devtools_app/test/shared/visible_screens_test.dart b/packages/devtools_app/test/shared/visible_screens_test.dart index 7823067629d..d0f9e2c1fb4 100644 --- a/packages/devtools_app/test/shared/visible_screens_test.dart +++ b/packages/devtools_app/test/shared/visible_screens_test.dart @@ -63,6 +63,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, // InspectorScreen, // LegacyPerformanceScreen, PerformanceScreen, @@ -83,6 +84,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, // InspectorScreen, // LegacyPerformanceScreen, // PerformanceScreen, @@ -105,6 +107,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, InspectorScreen, // LegacyPerformanceScreen, PerformanceScreen, @@ -128,6 +131,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, // InspectorScreen, // LegacyPerformanceScreen, PerformanceScreen, @@ -151,6 +155,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, InspectorScreen, // LegacyPerformanceScreen, // PerformanceScreen, @@ -185,6 +190,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, InspectorScreen, PerformanceScreen, ProfilerScreen, @@ -208,6 +214,7 @@ void main() { expect( visibleScreenTypes, equals([ + // HomeScreen, // InspectorScreen, PerformanceScreen, // Works offline, so appears regardless of web flag ProfilerScreen, // Works offline, so appears regardless of web flag @@ -230,6 +237,7 @@ void main() { expect( visibleScreenTypes, equals([ + HomeScreen, // InspectorScreen, // LegacyPerformanceScreen, PerformanceScreen, @@ -248,7 +256,7 @@ void main() { }); } -List get visibleScreenTypes => defaultScreens +List get visibleScreenTypes => defaultScreens() .map((s) => s.screen) .where(shouldShowScreen) .map((s) => s.runtimeType) diff --git a/packages/devtools_app/test/shared/vm_flag_widgets_test.dart b/packages/devtools_app/test/shared/vm_flag_widgets_test.dart index 4bc3064b3a7..1c175192f23 100644 --- a/packages/devtools_app/test/shared/vm_flag_widgets_test.dart +++ b/packages/devtools_app/test/shared/vm_flag_widgets_test.dart @@ -4,12 +4,15 @@ import 'package:collection/collection.dart'; import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app/src/service/service_registrations.dart' + as registrations; import 'package:devtools_app/src/service/vm_flags.dart' as vm_flags; import 'package:devtools_app/src/shared/ui/drop_down_button.dart'; import 'package:devtools_app/src/shared/ui/vm_flag_widgets.dart'; import 'package:devtools_test/devtools_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; import 'package:vm_service/vm_service.dart'; @@ -177,6 +180,48 @@ void main() { }, ); }); + + group('VMFlagsDialog', () { + late FakeServiceManager fakeServiceManager; + + void initServiceManager({ + bool flutterVersionServiceAvailable = true, + }) { + final availableServices = [ + if (flutterVersionServiceAvailable) + registrations.flutterVersion.service, + ]; + fakeServiceManager = FakeServiceManager( + availableServices: availableServices, + ); + when(fakeServiceManager.vm.version).thenReturn('1.9.1'); + final app = fakeServiceManager.connectedApp!; + when(app.isDartWebAppNow).thenReturn(false); + when(app.isRunningOnDartVM).thenReturn(true); + setGlobal(ServiceConnectionManager, fakeServiceManager); + } + + setUp(() { + initServiceManager(); + }); + + testWidgets('builds dialog', (WidgetTester tester) async { + mockConnectedApp( + fakeServiceManager.connectedApp!, + isFlutterApp: true, + isProfileBuild: false, + isWebApp: false, + ); + + await tester.pumpWidget(wrap(const VMFlagsDialog())); + expect(find.richText('VM Flags'), findsOneWidget); + expect(find.richText('flag 1 name'), findsOneWidget); + final RichText commentText = tester.firstWidget( + findSubstring('flag 1 comment'), + ); + expect(commentText, isNotNull); + }); + }); } BannerMessagesController bannerMessagesController(BuildContext context) { diff --git a/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart b/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart index 02eafd22247..4dc9e3d44c6 100644 --- a/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart +++ b/packages/devtools_test/lib/src/integration_test/integration_test_utils.dart @@ -28,12 +28,14 @@ Future pumpAndConnectDevTools( TestApp testApp, ) async { await pumpDevTools(tester); - expect(find.byType(LandingScreenBody), findsOneWidget); + expect(find.byType(ConnectDialog), findsOneWidget); + expect(find.byType(ConnectedAppSummary), findsNothing); expect(find.text('No client connection'), findsOneWidget); logStatus('verify that we can connect to an app'); await connectToTestApp(tester, testApp); - expect(find.byType(LandingScreenBody), findsNothing); + expect(find.byType(ConnectDialog), findsNothing); + expect(find.byType(ConnectedAppSummary), findsOneWidget); expect(find.text('No client connection'), findsNothing); // If the release notes viewer is open, close it. @@ -50,10 +52,8 @@ Future pumpAndConnectDevTools( } Future switchToScreen(WidgetTester tester, ScreenMetaData screen) async { - final screenTitle = screen.title; - logStatus('switching to $screenTitle screen'); - - final tabFinder = find.widgetWithText(Tab, screenTitle); + logStatus('switching to ${screen.name} screen (icon ${screen.icon})'); + final tabFinder = find.widgetWithIcon(Tab, screen.icon!); // If we cannot find the tab, try opening the tab overflow menu, if present. if (tabFinder.evaluate().isEmpty) { @@ -103,7 +103,12 @@ Future connectToTestApp(WidgetTester tester, TestApp testApp) async { } Future disconnectFromTestApp(WidgetTester tester) async { - await tester.tap(find.byTooltip(StatusLine.deviceInfoTooltip)); + await tester.tap( + find.descendant( + of: find.byType(DevToolsAppBar), + matching: find.byIcon(Icons.home_rounded), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byType(ConnectToNewAppButton)); await tester.pump(safePumpDuration);