blob: 37a27b4cc9c2e243fd53dd4adfe1855280c0b9c1 [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:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../primitives/listenable.dart';
import 'globals.dart';
import 'theme.dart';
import 'version.dart';
/// Defines a page shown in the DevTools [TabBar].
@immutable
abstract class Screen {
const Screen(
this.screenId, {
this.title = '',
this.icon,
this.tabKey,
this.requiresLibrary,
this.requiresDartVm = false,
this.requiresDebugBuild = false,
this.requiresVmDeveloperMode = false,
this.worksOffline = false,
this.shouldShowForFlutterVersion,
this.showFloatingDebuggerControls = true,
});
const Screen.conditional({
required String id,
String? requiresLibrary,
bool requiresDartVm = false,
bool requiresDebugBuild = false,
bool requiresVmDeveloperMode = false,
bool worksOffline = false,
bool Function(FlutterVersion? currentVersion)? shouldShowForFlutterVersion,
bool showFloatingDebuggerControls = true,
String title = '',
IconData? icon,
Key? tabKey,
}) : this(
id,
requiresLibrary: requiresLibrary,
requiresDartVm: requiresDartVm,
requiresDebugBuild: requiresDebugBuild,
requiresVmDeveloperMode: requiresVmDeveloperMode,
worksOffline: worksOffline,
shouldShowForFlutterVersion: shouldShowForFlutterVersion,
showFloatingDebuggerControls: showFloatingDebuggerControls,
title: title,
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.
final String title;
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 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 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 approximateWidth(TextTheme textTheme) {
final painter = TextPainter(
text: TextSpan(
text: title,
style: textTheme.bodyText1,
),
textDirection: TextDirection.ltr,
)..layout();
return painter.width + denseSpacing + defaultIconSize + defaultSpacing * 2;
}
/// 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) {
return ValueListenableBuilder<int>(
valueListenable:
serviceManager.errorBadgeManager.errorCountNotifier(screenId),
builder: (context, count, _) {
final tab = Tab(
key: tabKey,
child: Row(
children: <Widget>[
Icon(icon, size: defaultIconSize),
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),
),
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;
}
}
mixin OfflineScreenMixin<T extends StatefulWidget, U> on State<T> {
bool get loadingOfflineData => _loadingOfflineData;
bool _loadingOfflineData = false;
bool shouldLoadOfflineData();
FutureOr<void> processOfflineData(U offlineData);
Future<void> loadOfflineData(U offlineData) async {
setState(() {
_loadingOfflineData = true;
});
await processOfflineData(offlineData);
setState(() {
_loadingOfflineData = false;
});
}
}
/// Check whether a screen should be shown in the UI.
bool shouldShowScreen(Screen screen) {
if (offlineController.offlineMode.value) {
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.
if (!serviceManager.isServiceAvailable ||
!serviceManager.connectedApp!.connectedAppInitialized) return false;
if (screen.requiresLibrary != null) {
if (serviceManager.isolateManager.mainIsolate.value == null ||
!serviceManager.libraryUriAvailableNow(screen.requiresLibrary)) {
return false;
}
}
if (screen.requiresDartVm) {
if (serviceManager.connectedApp!.isRunningOnDartVM != true) {
return false;
}
}
if (screen.requiresDebugBuild) {
if (serviceManager.connectedApp!.isProfileBuildNow == true) {
return false;
}
}
if (screen.requiresVmDeveloperMode) {
if (!preferences.vmDeveloperModeEnabled.value) {
return false;
}
}
if (screen.shouldShowForFlutterVersion != null) {
if (serviceManager.connectedApp!.isFlutterAppNow == true &&
!screen.shouldShowForFlutterVersion!(
serviceManager.connectedApp!.flutterVersionNow,
)) {
return false;
}
}
return true;
}
class BadgePainter extends CustomPainter {
BadgePainter({required this.number});
final int number;
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = devtoolsError
..style = PaintingStyle.fill;
final countPainter = TextPainter(
text: TextSpan(
text: '$number',
style: const TextStyle(
color: Colors.white,
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;
}