blob: 201cac8314e71c01f569e47bdbda0ca837165411 [file] [log] [blame]
// 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 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
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),
deepLinks('deep-links', title: 'Deep Links', icon: Icons.link_rounded),
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.titleGenerator,
this.icon,
this.tabKey,
this.requiresLibrary,
this.requiresConnection = true,
this.requiresDartVm = false,
this.requiresFlutter = 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 requiresFlutter = false,
bool requiresDebugBuild = false,
bool requiresVmDeveloperMode = false,
bool worksOffline = false,
bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion,
bool showFloatingDebuggerControls = true,
String? title,
String Function()? titleGenerator,
IconData? icon,
Key? tabKey,
}) : this(
id,
requiresLibrary: requiresLibrary,
requiresConnection: requiresConnection,
requiresDartVm: requiresDartVm,
requiresFlutter: requiresFlutter,
requiresDebugBuild: requiresDebugBuild,
requiresVmDeveloperMode: requiresVmDeveloperMode,
worksOffline: worksOffline,
shouldShowForFlutterVersion: shouldShowForFlutterVersion,
showFloatingDebuggerControls: showFloatingDebuggerControls,
title: title,
titleGenerator: titleGenerator,
icon: icon,
tabKey: tabKey,
);
/// Whether to show floating debugger controls if the app is paused.
///
/// If your page is negatively impacted by the app being paused you should
/// show debugger controls.
final bool showFloatingDebuggerControls;
/// Whether to show the console for this screen.
bool showConsole(bool embed) => false;
/// Which keyboard shortcuts should be enabled for this screen.
ShortcutsConfiguration buildKeyboardShortcuts(BuildContext context) =>
ShortcutsConfiguration.empty();
final String screenId;
/// The user-facing name of the page.
///
/// 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;
/// An optional key to use when creating the Tab widget (for use during
/// testing).
final Key? tabKey;
/// Library uri that determines whether to include this screen in DevTools.
///
/// This can either be a full library uri or it can be a prefix. If null, this
/// screen will be shown if it meets all other criteria.
///
/// Examples:
/// * 'package:provider/provider.dart'
/// * '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;
/// Whether this screen should only be included when the app is a Flutter app.
final bool requiresFlutter;
/// Whether this screen should only be included when the app is debuggable.
final bool requiresDebugBuild;
/// Whether this screen should only be included when VM developer mode is enabled.
final bool requiresVmDeveloperMode;
/// Whether this screen works offline and should show in offline mode even if conditions are not met.
final bool worksOffline;
/// A callback that will determine whether or not this screen should be
/// available for a given flutter version.
final bool Function(FlutterVersion? currentFlutterVersion)?
shouldShowForFlutterVersion;
/// Whether this screen should display the isolate selector in the status
/// line.
///
/// Some screens act on all isolates; for these screens, displaying a
/// selector doesn't make sense.
ValueListenable<bool> get showIsolateSelector =>
const FixedValueListenable<bool>(false);
/// The id to use to synthesize a help URL.
///
/// If the screen does not have a custom documentation page, this property
/// should return `null`.
String? get docPageId => null;
int get badgeCount => 0;
double approximateTabWidth(
TextTheme textTheme, {
bool includeTabBarSpacing = true,
}) {
final title = _userFacingTitle;
final painter = TextPainter(
text: TextSpan(text: title),
textDirection: TextDirection.ltr,
)..layout();
const measurementBuffer = 2.0;
return painter.width +
denseSpacing +
defaultIconSize +
(includeTabBarSpacing ? tabBarSpacing * 2 : 0.0) +
// Add a small buffer to account for variances between the text painter
// approximation and the actual measurement.
measurementBuffer;
}
/// Builds the tab to show for this screen in the [DevToolsScaffold]'s main
/// navbar.
///
/// 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<int>(
valueListenable:
serviceManager.errorBadgeManager.errorCountNotifier(screenId),
builder: (context, count, _) {
final tab = Tab(
key: tabKey,
child: Row(
children: <Widget>[
Icon(icon, size: defaultIconSize),
if (title.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: denseSpacing),
child: Text(title),
),
],
),
);
if (count > 0) {
// Calculate the width of the title text so that we can provide an accurate
// size for the [BadgePainter]
final painter = TextPainter(
text: TextSpan(
text: title,
style: Theme.of(context).regularTextStyle,
),
textDirection: TextDirection.ltr,
)..layout();
final titleWidth = painter.width;
return LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
CustomPaint(
size: Size(defaultIconSize + denseSpacing + titleWidth, 0),
painter: BadgePainter(
number: count,
colorScheme: Theme.of(context).colorScheme,
),
),
tab,
],
);
},
);
}
return tab;
},
);
}
/// Builds the body to display for this tab.
Widget build(BuildContext context);
/// Build a widget to display in the status line.
///
/// If this method returns `null`, then no page specific status is displayed.
Widget? buildStatus(BuildContext context) {
return null;
}
}
/// Check whether a screen should be shown in the UI.
bool shouldShowScreen(Screen screen) {
_log.finest('shouldShowScreen: ${screen.screenId}');
if (offlineController.offlineMode.value) {
_log.finest('for offline mode: returning ${screen.worksOffline}');
return screen.worksOffline;
}
final serviceReady = serviceManager.isServiceAvailable &&
serviceManager.connectedApp!.connectedAppInitialized;
if (!serviceReady) {
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) {
if (serviceManager.isolateManager.mainIsolate.value == null ||
!serviceManager.libraryUriAvailableNow(screen.requiresLibrary)) {
_log.finest(
'screen requires library ${screen.requiresLibrary}: returning false',
);
return false;
}
}
if (screen.requiresDartVm) {
if (serviceManager.connectedApp!.isRunningOnDartVM != true) {
_log.finest('screen requires Dart VM: returning false');
return false;
}
}
if (screen.requiresFlutter &&
serviceManager.connectedApp!.isFlutterAppNow == false) {
_log.finest('screen requires Flutter: returning false');
return false;
}
if (screen.requiresDebugBuild) {
if (serviceManager.connectedApp!.isProfileBuildNow == true) {
_log.finest('screen requires debug build: returning false');
return false;
}
}
if (screen.requiresVmDeveloperMode) {
if (!preferences.vmDeveloperModeEnabled.value) {
_log.finest('screen requires vm developer mode: returning false');
return false;
}
}
if (screen.shouldShowForFlutterVersion != null) {
if (serviceManager.connectedApp!.isFlutterAppNow == true &&
!screen.shouldShowForFlutterVersion!(
serviceManager.connectedApp!.flutterVersionNow,
)) {
_log.finest('screen has flutter version restraints: returning false');
return false;
}
}
_log.finest('${screen.screenId} screen supported: returning true');
return true;
}
class BadgePainter extends CustomPainter {
BadgePainter({required this.number, required this.colorScheme});
final ColorScheme colorScheme;
final int number;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = colorScheme.errorContainer
..style = PaintingStyle.fill;
final countPainter = TextPainter(
text: TextSpan(
text: '$number',
style: TextStyle(
color: colorScheme.onErrorContainer,
fontWeight: FontWeight.bold,
),
),
textDirection: TextDirection.ltr,
)..layout();
final badgeWidth = math.max(
defaultIconSize,
countPainter.width + denseSpacing,
);
canvas.drawOval(
Rect.fromLTWH(size.width, 0, badgeWidth, defaultIconSize),
paint,
);
countPainter.paint(
canvas,
Offset(size.width + (badgeWidth - countPainter.width) / 2, 0),
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
if (oldDelegate is BadgePainter) {
return number != oldDelegate.number;
}
return true;
}
}
class ShortcutsConfiguration {
const ShortcutsConfiguration({
required this.shortcuts,
required this.actions,
}) : assert(shortcuts.length == actions.length);
factory ShortcutsConfiguration.empty() {
return ShortcutsConfiguration(shortcuts: {}, actions: {});
}
final Map<ShortcutActivator, Intent> shortcuts;
final Map<Type, Action<Intent>> actions;
bool get isEmpty => shortcuts.isEmpty && actions.isEmpty;
}