Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/devtools_app/lib/devtools_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export 'src/screens/deep_link_validation/deep_links_screen.dart';
export 'src/screens/dtd/dtd_tools_controller.dart';
export 'src/screens/dtd/dtd_tools_screen.dart';
export 'src/screens/inspector/inspector_controller.dart';
export 'src/screens/inspector/inspector_errors.dart';
export 'src/screens/inspector/inspector_screen.dart';
export 'src/screens/inspector/inspector_screen_body.dart';
export 'src/screens/inspector/inspector_screen_controller.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ import '../../shared/console/primitives/simple_items.dart';
import '../../shared/diagnostics/diagnostics_node.dart';
import '../../shared/diagnostics/inspector_service.dart';
import '../../shared/diagnostics/primitives/instance_ref.dart';
import '../../shared/framework/screen_controllers.dart';
import '../../shared/globals.dart';
import '../../shared/managers/notifications.dart';
import '../../shared/primitives/query_parameters.dart';
import '../../shared/primitives/utils.dart';
import '../../shared/utils/utils.dart';
import 'inspector_data_models.dart';
import 'inspector_screen.dart';
import 'inspector_screen_controller.dart';
import 'inspector_tree_controller.dart';

final _log = Logger('inspector_controller');
Expand Down Expand Up @@ -149,6 +151,21 @@ class InspectorController extends DisposableController
}
}

/// Returns the [InspectorScreenController] when it is registered.
///
/// [InspectorController] is sometimes constructed in unit tests without an
/// [InspectorScreenController] registered, so callers that only need to clear
/// errors on connect/reload should use this nullable accessor.
InspectorScreenController? get _inspectorScreenControllerOrNull {
final controllers = globals[ScreenControllers] as ScreenControllers?;
if (controllers == null) return null;
if (!controllers.isRegistered<InspectorScreenController>()) return null;
return controllers.lookup<InspectorScreenController>();
}

InspectorScreenController get _inspectorScreenController =>
screenControllers.lookup<InspectorScreenController>();

void _handleConnectionStart() {
// Clear any existing badge/errors for older errors that were collected.
// Do this in a post frame callback so that we are not trying to clear the
Expand All @@ -157,7 +174,7 @@ class InspectorController extends DisposableController
// TODO(kenz): When this method is called outside createState(), this post
// frame callback can be removed.
WidgetsBinding.instance.addPostFrameCallback((_) {
serviceConnection.errorBadgeManager.clearErrors(InspectorScreen.id);
_inspectorScreenControllerOrNull?.clearErrors();
});
}

Expand Down Expand Up @@ -467,7 +484,7 @@ class InspectorController extends DisposableController
}

if (event.kind == EventKind.kIsolateReload) {
serviceConnection.errorBadgeManager.clearErrors(InspectorScreen.id);
_inspectorScreenControllerOrNull?.clearErrors();
_receivedIsolateReloadEvent = true;
}
}
Expand Down Expand Up @@ -952,9 +969,7 @@ class InspectorController extends DisposableController
void _updateSelectedErrorFromNode(InspectorTreeNode? node) {
final inspectorRef = node?.diagnostic?.valueRef.id;

final errors = serviceConnection.errorBadgeManager
.erroredItemsForPage(InspectorScreen.id)
.value;
final errors = _inspectorScreenController.inspectorErrors.value;

// Check whether the node that was just selected has any errors associated
// with it.
Expand All @@ -970,20 +985,15 @@ class InspectorController extends DisposableController
if (errorIndex != null) {
// Marking an error as read will automatically update the badge count to
// reflect the remaining unread errors.
serviceConnection.errorBadgeManager.markErrorAsRead(
InspectorScreen.id,
errors[inspectorRef!]!,
);
_inspectorScreenController.markErrorAsRead(errors[inspectorRef!]!);
}
}

/// Updates the index of the selected error and selects its node in the tree.
void selectErrorByIndex(int index) {
_selectedErrorIndex.value = index;

final errors = serviceConnection.errorBadgeManager
.erroredItemsForPage(InspectorScreen.id)
.value;
final errors = _inspectorScreenController.inspectorErrors.value;

unawaited(
updateSelectionFromService(inspectorRef: errors.keys.elementAt(index)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 The Flutter Authors
// 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 '../../shared/managers/error_badge_manager.dart';

/// An error associated with a specific widget that can be inspected in the
/// Inspector screen.
class InspectableWidgetError extends DevToolsError {
InspectableWidgetError(super.errorMessage, super.id, {super.read});

@override
InspectableWidgetError asRead() =>
InspectableWidgetError(errorMessage, id, read: true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import '../../shared/ui/search.dart';
import '../../shared/utils/utils.dart';
import 'inspector_controller.dart';
import 'inspector_controls.dart';
import 'inspector_errors.dart';
import 'inspector_screen.dart';
import 'inspector_screen_controller.dart';
import 'inspector_tree_controller.dart';
import 'widget_details.dart';

Expand Down Expand Up @@ -149,8 +151,9 @@ class InspectorScreenBodyState extends State<InspectorScreenBody>
),
Expanded(
child: ValueListenableBuilder(
valueListenable: serviceConnection.errorBadgeManager
.erroredItemsForPage(InspectorScreen.id),
valueListenable: screenControllers
.lookup<InspectorScreenController>()
.inspectorErrors,
builder: (_, LinkedHashMap<String, DevToolsError> errors, _) {
final inspectableErrors =
errors.map(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,26 @@
// 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:collection';
Comment thread
khanak0509 marked this conversation as resolved.

import 'package:collection/collection.dart' show IterableExtension;
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:flutter/foundation.dart';
import 'package:vm_service/vm_service.dart';

import '../../service/vm_service_wrapper.dart';
import '../../shared/analytics/metrics.dart';
import '../../shared/console/primitives/simple_items.dart';
import '../../shared/diagnostics/diagnostics_node.dart';
import '../../shared/framework/screen.dart';
import '../../shared/framework/screen_controllers.dart';
import '../../shared/globals.dart';
import '../../shared/managers/error_badge_manager.dart';
import '../../shared/primitives/query_parameters.dart';
import 'inspector_controller.dart';
import 'inspector_errors.dart';
import 'inspector_screen.dart';
import 'inspector_tree_controller.dart';

/// Screen controller for the Inspector screen.
Expand All @@ -19,27 +34,164 @@ import 'inspector_tree_controller.dart';
/// `init` method is called lazily upon the first controller access from
/// `screenControllers`. The `dispose` method is called by `screenControllers`
/// when DevTools is destroying a set of DevTools screen controllers.
class InspectorScreenController extends DevToolsScreenController {
class InspectorScreenController extends DevToolsScreenController
with AutoDisposeControllerMixin {
@override
final screenId = ScreenMetaData.inspector.id;

late InspectorController inspectorController;
late InspectorTreeController inspectorTreeController;

/// Stores the inspector-specific errors keyed by inspector reference ID.
final _activeInspectorErrors =
ValueNotifier<LinkedHashMap<String, DevToolsError>>(
LinkedHashMap<String, DevToolsError>(),
);
Comment thread
khanak0509 marked this conversation as resolved.

/// The errors currently tracked for the inspector screen.
ValueListenable<LinkedHashMap<String, DevToolsError>> get inspectorErrors =>
_activeInspectorErrors;

/// The count of unread inspector errors (used for the badge).
ValueListenable<int> get inspectorErrorCount => serviceConnection
.errorBadgeManager
.errorCountNotifier(InspectorScreen.id);

@override
void init() {
super.init();
// Inspector owns unread state for its badge; scaffold tab switches must not
// clear it. See https://github.com/flutter/devtools/pull/9805.
serviceConnection.errorBadgeManager.manageErrorCount(InspectorScreen.id);

inspectorTreeController = InspectorTreeController(
gaId: InspectorScreenMetrics.summaryTreeGaId,
);
inspectorController = InspectorController(
inspectorTree: inspectorTreeController,
treeType: FlutterTreeType.widget,
);

// Listen for Flutter extension events to extract inspector-specific errors.
// Match other screen controllers: attach now if connected, and on connect.
addAutoDisposeListener(serviceConnection.serviceManager.connectedState, () {
if (serviceConnection.serviceManager.connectedState.value.connected) {
_handleConnectionStart(serviceConnection.serviceManager.service!);
}
});
if (serviceConnection.serviceManager.connectedAppInitialized) {
_handleConnectionStart(serviceConnection.serviceManager.service!);
}
}

void _handleConnectionStart(VmServiceWrapper service) {
autoDisposeStreamSubscription(
service.onExtensionEventWithHistorySafe.listen(_handleExtensionEvent),
);
}
Comment thread
khanak0509 marked this conversation as resolved.

void _handleExtensionEvent(Event e) {
if (e.extensionKind == FlutterEvent.error) {
final inspectableError = _extractInspectableError(e);
if (inspectableError != null) {
appendError(inspectableError);
}
}
}

InspectableWidgetError? _extractInspectableError(Event error) {
final extensionData = error.extensionData;
if (extensionData == null) return null;

final node = RemoteDiagnosticsNode(extensionData.data, null, false, null);

final errorSummaryNode = node.inlineProperties.firstWhereOrNull(
(p) => p.type == 'ErrorSummary',
);
final errorMessage = errorSummaryNode?.description;
if (errorMessage == null) {
return null;
}

final devToolsUrlNode = node.inlineProperties.firstWhereOrNull(
(p) =>
p.type == 'DevToolsDeepLinkProperty' &&
p.getStringMember('value') != null,
);
if (devToolsUrlNode == null) {
return null;
}

final queryParams = DevToolsQueryParams.fromUrl(
devToolsUrlNode.getStringMember('value')!,
);
final inspectorRef = queryParams.inspectorRef ?? '';

return InspectableWidgetError(errorMessage, inspectorRef);
}

/// Appends an error to the inspector's active errors and updates the badge
/// count.
void appendError(DevToolsError error) {
final errors = _activeInspectorErrors;
final previousError = errors.value[error.id];

// Build a new map with the new error. Adding to the existing map
// won't cause the ValueNotifier to fire (and it's not permitted to call
// notifyListeners() directly).
final newValue = LinkedHashMap<String, DevToolsError>.of(errors.value);
newValue[error.id] = error;
errors.value = newValue;

if (previousError == null) {
if (!error.read) {
_incrementUnreadCount();
}
return;
}

if (previousError.read && !error.read) {
_incrementUnreadCount();
} else if (!previousError.read && error.read) {
_decrementUnreadCount();
}
}

/// Clears all inspector errors and resets the badge count.
void clearErrors() {
_activeInspectorErrors.value = LinkedHashMap<String, DevToolsError>();
serviceConnection.errorBadgeManager.resetErrorCount(InspectorScreen.id);
}

/// Marks an error as read and decrements the unread count.
void markErrorAsRead(DevToolsError error) {
final errors = _activeInspectorErrors;

// If this error doesn't exist anymore or is already read, nothing to do.
final currentError = errors.value[error.id];
if (currentError == null || currentError.read) {
return;
}

// Otherwise, replace the map with a new one that has the error marked
// as read.
final newValue = LinkedHashMap<String, DevToolsError>.of(errors.value);
newValue[error.id] = currentError.asRead();
errors.value = newValue;
_decrementUnreadCount();
}
Comment thread
khanak0509 marked this conversation as resolved.

void _incrementUnreadCount() {
serviceConnection.errorBadgeManager.incrementBadgeCount(InspectorScreen.id);
}

void _decrementUnreadCount() {
serviceConnection.errorBadgeManager.decrementBadgeCount(InspectorScreen.id);
}

@override
void dispose() {
_activeInspectorErrors.dispose();
inspectorTreeController.dispose();
inspectorController.dispose();
super.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import '../../shared/ui/search.dart';
import '../../shared/ui/utils.dart';
import '../../shared/utils/utils.dart';
import 'inspector_controller.dart';
import 'inspector_errors.dart';

final _log = Logger('inspector_tree_controller');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class ScreenControllers {
controllers[T] = _LazyController<T>(creator: controllerCreator);
}

/// Whether a controller of type [T] has been registered for the active mode.
bool isRegistered<T>() {
final controllers = offlineDataController.showingOfflineData.value
? offlineControllers
: this.controllers;
return controllers.containsKey(T);
}

/// Returns the active screen controller of type [T].
///
/// When DevTools is showing offline data, the offline screen controller will
Expand Down
Loading
Loading