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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import '../../shared/globals.dart';
import '../../shared/managers/banner_messages.dart';
import '../../shared/primitives/query_parameters.dart';
import '../../shared/title.dart';
import '../../shared/ui/common_widgets.dart';
import 'about_dialog.dart';
import 'app_bar.dart';
import 'bottom_pane.dart';
Expand Down Expand Up @@ -59,7 +60,7 @@ class DevToolsScaffold extends StatefulWidget {
);

/// Returns the list of ScaffoldAction widgets.
static List<Widget> defaultActions({
static List<ScaffoldAction> defaultActions({
Color? color,
Screen? currentScreen,
DevToolsQueryParams? queryParams,
Expand Down
157 changes: 125 additions & 32 deletions packages/devtools_app/lib/src/framework/scaffold/status_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:async';

import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/ui.dart';
import 'package:material_ui/material_ui.dart';
import 'package:vm_service/vm_service.dart';

import '../../shared/analytics/analytics.dart' as ga;
import '../../shared/analytics/constants.dart' as gac;
import '../../shared/framework/screen.dart';
import '../../shared/globals.dart';
Expand Down Expand Up @@ -57,8 +60,12 @@ class StatusLine extends StatelessWidget {
valueListenable: currentScreen.showIsolateSelector,
builder: (context, showIsolateSelector, _) {
showIsolateSelector = showIsolateSelector && isConnected;
// Keep every footer item on a single line. Items that do not fit are
// truncated rather than overflowing the status line.
return DefaultTextStyle.merge(
style: TextStyle(color: foregroundColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
Expand Down Expand Up @@ -93,6 +100,24 @@ class StatusLine extends StatelessWidget {
final showVideoTutorial =
screenMetaData?.tutorialVideoTimestamp != null ||
screenMetaData?.tutorialVideoUrl != null;
// Secondary items (the tutorial link and, when embedded, the DevTools
// actions) are shown inline only on wide screens. Below [MediaSize.s] they
// move into one overflow menu, so they stay reachable and the device name
// and page status are not squeezed.
final showSecondaryItemsInline = screenWidth > MediaSize.s;
final showTutorialInline = showVideoTutorial && showSecondaryItemsInline;
final embeddedActions = isEmbedded
? DevToolsScaffold.defaultActions(
color: foregroundColor,
currentScreen: currentScreen,
)
: const <ScaffoldAction>[];
final overflowTutorial = showVideoTutorial && !showTutorialInline
? screenMetaData
: null;
final overflowActions = showSecondaryItemsInline
? const <ScaffoldAction>[]
: embeddedActions;
return [
Row(
mainAxisSize: MainAxisSize.min,
Expand All @@ -102,11 +127,10 @@ class StatusLine extends StatelessWidget {
screenWidth: screenWidth,
highlightForConnection: highlightForConnection,
),
if (showVideoTutorial) ...[
if (showTutorialInline) ...[
BulletSpacer(color: foregroundColor),
VideoTutorialLink(
screenMetaData: screenMetaData!,
screenWidth: screenWidth,
highlightForConnection: highlightForConnection,
),
],
Expand All @@ -118,18 +142,28 @@ class StatusLine extends StatelessWidget {
BulletSpacer(color: foregroundColor),
],
if (screenWidth > MediaSize.xs && pageStatus != null) ...[
pageStatus,
// The page status needs to be flexible so that the status line can
// shrink to fit narrow screens without overflowing.
Expanded(child: pageStatus),
BulletSpacer(color: foregroundColor),
],
buildConnectionStatus(context, screenWidth),
if (widerThanXxs && isEmbedded) ...[
// The connection status needs to be flexible so that the status line
// can shrink to fit narrow screens without overflowing.
Flexible(child: buildConnectionStatus(context, screenWidth)),
if (showSecondaryItemsInline && embeddedActions.isNotEmpty) ...[
BulletSpacer(color: foregroundColor),
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: DevToolsScaffold.defaultActions(
color: foregroundColor,
currentScreen: currentScreen,
),
children: embeddedActions,
),
],
if (overflowTutorial != null || overflowActions.isNotEmpty) ...[
BulletSpacer(color: foregroundColor),
_StatusLineOverflowMenu(
tutorial: overflowTutorial,
actions: overflowActions,
color: foregroundColor,
),
],
];
Expand Down Expand Up @@ -174,16 +208,18 @@ class StatusLine extends StatelessWidget {
},
),
const SizedBox(width: denseSpacing),
DevToolsTooltip(
message: 'Connected device',
child: Text(
description,
style: highlightForConnection
? theme.regularTextStyle.copyWith(
color: theme.colorScheme.onPrimary,
)
: theme.regularTextStyle,
overflow: TextOverflow.clip,
Flexible(
child: DevToolsTooltip(
message: 'Connected device',
child: Text(
description,
style: highlightForConnection
? theme.regularTextStyle.copyWith(
color: theme.colorScheme.onPrimary,
)
: theme.regularTextStyle,
overflow: TextOverflow.ellipsis,
),
),
),
],
Expand All @@ -192,7 +228,7 @@ class StatusLine extends StatelessWidget {
return child!;
}
},
child: screenWidth <= MediaSize.xxs
child: screenWidth <= MediaSize.xs
? const DevToolsTooltip(
message: noConnectionMsg,
child: Icon(Icons.warning_amber_rounded, size: actionsIconSize),
Expand Down Expand Up @@ -245,7 +281,6 @@ class VideoTutorialLink extends StatelessWidget {
VideoTutorialLink({
super.key,
required this.screenMetaData,
required this.screenWidth,
required this.highlightForConnection,
}) : assert(
screenMetaData.tutorialVideoTimestamp != null ||
Expand All @@ -254,33 +289,91 @@ class VideoTutorialLink extends StatelessWidget {

final ScreenMetaData screenMetaData;

final MediaSize screenWidth;

final bool highlightForConnection;

static const _devToolsYouTubeVideoUrl = 'https://youtu.be/_EYk-E29edo';

static GaLink _linkFor(ScreenMetaData screenMetaData) {
return GaLink(
display: 'Watch tutorial',
url:
screenMetaData.tutorialVideoUrl ??
'$_devToolsYouTubeVideoUrl${screenMetaData.tutorialVideoTimestamp}',
gaScreenName: screenMetaData.id,
gaSelectedItemDescription:
'${gac.videoTutorialLink}-${screenMetaData.id}',
);
}

/// Opens the tutorial video for [screenMetaData] and records the same
/// analytics event as this link, for use from the overflow menu.
static void open(ScreenMetaData screenMetaData) {
final link = _linkFor(screenMetaData);
unawaited(launchUrlWithErrorHandling(link.url));
ga.select(link.gaScreenName!, link.gaSelectedItemDescription!);
}

@override
Widget build(BuildContext context) {
final color = highlightForConnection
? Theme.of(context).colorScheme.onPrimary
: null;
return LinkIconLabel(
icon: Icons.ondemand_video_rounded,
link: GaLink(
display: screenWidth <= MediaSize.xs ? 'Tutorial' : 'Watch tutorial',
url:
screenMetaData.tutorialVideoUrl ??
'$_devToolsYouTubeVideoUrl${screenMetaData.tutorialVideoTimestamp}',
gaScreenName: screenMetaData.id,
gaSelectedItemDescription:
'${gac.videoTutorialLink}-${screenMetaData.id}',
),
link: _linkFor(screenMetaData),
color: color,
);
}
}

/// The footer items that do not fit at the current width: the video tutorial
/// link and, for embedded DevTools, the DevTools actions (Settings, Feedback,
/// About, and extension settings), in one menu.
///
/// Each item runs the same behavior as its inline counterpart.
class _StatusLineOverflowMenu extends StatelessWidget {
const _StatusLineOverflowMenu({
required this.tutorial,
required this.actions,
required this.color,
});

/// The metadata of the screen whose tutorial should be offered, if any.
final ScreenMetaData? tutorial;

final List<ScaffoldAction> actions;

final Color? color;

@override
Widget build(BuildContext context) {
final tutorial = this.tutorial;
return PopupMenuButton<VoidCallback>(
tooltip: 'More actions',
padding: EdgeInsets.zero,
onSelected: (onSelected) => onSelected(),
itemBuilder: (_) => [
if (tutorial != null)
PopupMenuItem(
value: () => VideoTutorialLink.open(tutorial),
child: const Text('Watch tutorial'),
),
for (final action in actions)
PopupMenuItem(
value: () => action.onPressed(context),
child: Text(action.tooltip),
),
],
child: SizedBox(
width: actionWidgetSize,
child: Center(
child: Icon(Icons.more_horiz, size: actionsIconSize, color: color),
),
),
);
}
}

class IsolateSelector extends StatelessWidget {
const IsolateSelector({super.key, required this.foregroundColor});

Expand Down
65 changes: 46 additions & 19 deletions packages/devtools_app/lib/src/screens/network/network_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@ class NetworkScreen extends Screen {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Showing ${nf.format(filteredCount)} of '
'${nf.format(totalCount)} '
'${pluralize('request', totalCount)}',
Flexible(
child: Text(
'Showing ${nf.format(filteredCount)} of '
'${nf.format(totalCount)} '
'${pluralize('request', totalCount)}',
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: denseSpacing),
child!,
Expand Down Expand Up @@ -202,6 +205,24 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
}

final screenWidth = ScreenSize(context).width;
// The search field's navigation controls and the filter field's prefix and
// suffix controls need more width than is left beside the other controls
// on narrow screens. Below [MediaSize.s] the filter field moves to its own
// row, and below [MediaSize.xs] the search field does too. The threshold
// for the filter row is [MediaSize.s] because the filter field's suffix
// controls overflowed by 16 pixels at a 600 pixel width.
final searchOnOwnRow = screenWidth <= MediaSize.xs;
final filterOnOwnRow = screenWidth <= MediaSize.s;
final searchField = SearchField<NetworkController>(
searchController: controller,
searchFieldWidth: screenWidth <= MediaSize.xs
? defaultSearchFieldWidth
: wideSearchFieldWidth,
);
final filterField = StandaloneFilterField<NetworkRequest>(
controller: controller,
filteredItem: 'request',
);
return Column(
children: [
Row(
Expand All @@ -226,21 +247,14 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
),
const SizedBox(width: denseSpacing),
// TODO(kenz): fix focus issue when state is refreshed
Expanded(
child: SearchField<NetworkController>(
searchController: controller,
searchFieldWidth: screenWidth <= MediaSize.xs
? defaultSearchFieldWidth
: wideSearchFieldWidth,
),
),
const SizedBox(width: denseSpacing),
Expanded(
child: StandaloneFilterField<NetworkRequest>(
controller: controller,
filteredItem: 'request',
),
),
if (searchOnOwnRow)
const Spacer()
else
Expanded(child: searchField),
if (!filterOnOwnRow) ...[
const SizedBox(width: denseSpacing),
Expanded(child: filterField),
],
const SizedBox(width: denseSpacing),
OpenSaveButtonGroup(
screenId: ScreenMetaData.network.id,
Expand All @@ -262,6 +276,19 @@ class _NetworkProfilerControlsState extends State<_NetworkProfilerControls>
),
],
),
// On narrow screens, place the search and filter fields on their own
// rows so that each has enough space for its navigation, prefix, and
// suffix controls without overflowing.
if (searchOnOwnRow)
Padding(
padding: const EdgeInsets.only(top: denseSpacing),
child: Row(children: [Expanded(child: searchField)]),
),
if (filterOnOwnRow)
Padding(
padding: const EdgeInsets.only(top: denseSpacing),
child: filterField,
),
if (!_recording)
const Padding(
padding: EdgeInsets.only(top: intermediateSpacing),
Expand Down
6 changes: 5 additions & 1 deletion packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ TODO: Remove this section if there are not any updates.

## Network profiler updates

TODO: Remove this section if there are not any updates.
* Fixed an issue where the Network screen's toolbar controls and footer status
bar would overflow and overlap on narrow screens. On compact widths, the
video tutorial link and the embedded DevTools actions moved into the
More actions menu in the footer.
[#10004](https://github.com/flutter/devtools/issues/10004)

## Logging updates

Expand Down
Loading
Loading