diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart index fa8347bbf95..3ac1b594452 100644 --- a/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart +++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/debug_sessions.dart @@ -24,45 +24,28 @@ class DebugSessions extends StatelessWidget { children: [ Text( 'Debug Sessions', - style: Theme.of(context).textTheme.titleSmall, + style: Theme.of(context).textTheme.titleMedium, ), if (sessions.isEmpty) const Text('Begin a debug session to use DevTools.') else Table( columnWidths: const { - 0: FlexColumnWidth(2), - // TODO(dantup): Fixed width icons+menu? - 1: FlexColumnWidth(), + 0: FlexColumnWidth(), }, + defaultColumnWidth: + FixedColumnWidth(actionsIconSize + denseSpacing), defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ for (final session in sessions) - TableRow( - children: [ - Text( - '${session.name} (${session.flutterMode})', - style: Theme.of(context).textTheme.titleSmall, - ), - if (api.capabilities.openDevToolsPage) - _DevToolsMenu(api: api, session: session), - ], - ), + _debugSessionRow(session, context), ], ), ], ); } -} - -class _DevToolsMenu extends StatelessWidget { - const _DevToolsMenu({required this.api, required this.session}); - final VsCodeApi api; - final VsCodeDebugSession session; - - @override - Widget build(BuildContext context) { + TableRow _debugSessionRow(VsCodeDebugSession session, BuildContext context) { // TODO(dantup): What to show if mode is unknown (null)? final mode = session.flutterMode; final isDebug = mode == 'debug'; @@ -70,8 +53,16 @@ class _DevToolsMenu extends StatelessWidget { // final isRelease = mode == 'release' || mode == 'jit_release'; final isFlutter = session.debuggerType?.contains('Flutter') ?? false; - return MenuBar( + final label = session.flutterMode != null + ? '${session.name} (${session.flutterMode})' + : session.name; + + return TableRow( children: [ + Text( + label, + style: Theme.of(context).regularTextStyle, + ), IconButton( onPressed: api.capabilities.hotReload && (isDebug || !isFlutter) ? () => unawaited(api.hotReload(session.id)) @@ -86,53 +77,118 @@ class _DevToolsMenu extends StatelessWidget { tooltip: 'Hot Restart', icon: Icon(hotRestartIcon, size: actionsIconSize), ), - SubmenuButton( - menuChildren: [ - // TODO(dantup): Ensure the order matches the DevTools tab bar (if - // possible, share this order). - // TODO(dantup): Make these conditions use the real screen - // conditions and/or verify if these conditions are correct. - _devToolsButton( - ScreenMetaData.inspector, - enabled: isFlutter && isDebug, - ), - _devToolsButton( - ScreenMetaData.cpuProfiler, - enabled: isDebug || isProfile, - ), - _devToolsButton( - ScreenMetaData.memory, - enabled: isDebug || isProfile, - ), - _devToolsButton( - ScreenMetaData.performance, - ), - _devToolsButton( - ScreenMetaData.network, - enabled: isDebug, - ), - _devToolsButton( - ScreenMetaData.logging, - ), - // TODO(dantup): Check other screens (like appSize) work embedded and - // add here. - ], - child: const Text('DevTools'), - ), + if (api.capabilities.openDevToolsPage) + _DevToolsMenu( + api: api, + session: session, + isFlutter: isFlutter, + isDebug: isDebug, + isProfile: isProfile, + ), ], ); } +} + +class _DevToolsMenu extends StatelessWidget { + const _DevToolsMenu({ + required this.api, + required this.session, + required this.isFlutter, + required this.isDebug, + required this.isProfile, + }); + + final VsCodeApi api; + final VsCodeDebugSession session; + final bool isFlutter; + final bool isDebug; + final bool isProfile; + + @override + Widget build(BuildContext context) { + final normalDirection = Directionality.of(context); + final reversedDirection = normalDirection == TextDirection.ltr + ? TextDirection.rtl + : TextDirection.ltr; + + Widget devToolsButton( + ScreenMetaData screen, { + bool enabled = true, + }) { + return SizedBox( + width: double.infinity, + child: TextButton.icon( + style: TextButton.styleFrom( + alignment: Alignment.centerRight, + shape: const ContinuousRectangleBorder(), + ), + onPressed: enabled + ? () => unawaited(api.openDevToolsPage(session.id, screen.id)) + : null, + label: Directionality( + textDirection: normalDirection, + child: Text(screen.title ?? screen.id), + ), + icon: Icon(screen.icon, size: actionsIconSize), + ), + ); + } - Widget _devToolsButton( - ScreenMetaData screen, { - bool enabled = true, - }) { - return IconButton( - onPressed: enabled - ? () => unawaited(api.openDevToolsPage(session.id, screen.id)) - : null, - tooltip: screen.title ?? screen.id, - icon: Icon(screen.icon, size: actionsIconSize), + return Directionality( + // Reverse the direction so the menu is anchored on the far side and + // expands in the opposite direction with the icons on the right. + textDirection: reversedDirection, + child: MenuAnchor( + style: const MenuStyle( + alignment: AlignmentDirectional.bottomStart, + ), + menuChildren: [ + // TODO(dantup): Ensure the order matches the DevTools tab bar (if + // possible, share this order). + // TODO(dantup): Make these conditions use the real screen + // conditions and/or verify if these conditions are correct. + devToolsButton( + ScreenMetaData.inspector, + enabled: isFlutter && isDebug, + ), + devToolsButton( + ScreenMetaData.cpuProfiler, + enabled: isDebug || isProfile, + ), + devToolsButton( + ScreenMetaData.memory, + enabled: isDebug || isProfile, + ), + devToolsButton( + ScreenMetaData.performance, + ), + devToolsButton( + ScreenMetaData.network, + enabled: isDebug, + ), + devToolsButton( + ScreenMetaData.logging, + ), + // TODO(dantup): Check other screens (like appSize) work embedded and + // add here. + ], + builder: (context, controller, child) => IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + tooltip: 'DevTools', + // TODO(dantup): Icon for DevTools menu? + icon: Icon( + Icons.construction, + size: actionsIconSize, + ), + ), + ), ); } } diff --git a/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart b/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart index 4aac607a3b4..5b51670014f 100644 --- a/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart +++ b/packages/devtools_app/lib/src/standalone_ui/vs_code/devices.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:devtools_app_shared/ui.dart'; import 'package:flutter/material.dart'; import '../api/vs_code_api.dart'; @@ -22,25 +23,23 @@ class Devices extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Devices', - style: Theme.of(context).textTheme.titleSmall, + style: theme.textTheme.titleMedium, ), if (devices.isEmpty) const Text('Connect a device or enable web/desktop platforms.') else Table( - columnWidths: const { - 0: FlexColumnWidth(3), - 1: FlexColumnWidth(), - }, defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ for (final device in devices) _createDeviceRow( + theme, device, isSelected: device.id == selectedDeviceId, ), @@ -50,18 +49,32 @@ class Devices extends StatelessWidget { ); } - TableRow _createDeviceRow(VsCodeDevice device, {required bool isSelected}) { + TableRow _createDeviceRow( + ThemeData theme, + VsCodeDevice device, { + required bool isSelected, + }) { + final backgroundColor = isSelected ? theme.colorScheme.primary : null; + final foregroundColor = isSelected ? theme.colorScheme.onPrimary : null; + return TableRow( + decoration: BoxDecoration(color: backgroundColor), children: [ - Align( - alignment: Alignment.centerLeft, + SizedBox( + width: double.infinity, child: TextButton( - child: Text(device.name), + style: TextButton.styleFrom( + alignment: Alignment.centerLeft, + shape: const ContinuousRectangleBorder(), + textStyle: theme.regularTextStyle, + ), + child: Text( + device.name, + style: theme.regularTextStyle.copyWith(color: foregroundColor), + ), onPressed: () => unawaited(api.selectDevice(device.id)), ), ), - // TODO(dantup): Use a highlighted/select row for this instead of text. - Text(isSelected ? 'current device' : ''), ], ); } diff --git a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart index d245afcb30e..e4117e96e55 100644 --- a/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart +++ b/packages/devtools_app/test/test_infra/test_data/dart_tooling_api/mock_api.dart @@ -74,6 +74,8 @@ class MockDartToolingApi extends DartToolingApiImpl { server.registerMethod('vsCode.executeCommand', executeCommand); server.registerMethod('vsCode.selectDevice', selectDevice); server.registerMethod('vsCode.openDevToolsPage', noOpHandler); + server.registerMethod('vsCode.hotReload', noOpHandler); + server.registerMethod('vsCode.hotRestart', noOpHandler); } final json_rpc_2.Peer client;