22// Use of this source code is governed by a BSD-style license that can be
33// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
44
5+ import 'dart:collection' ;
6+
7+ import 'package:collection/collection.dart' show IterableExtension;
8+ import 'package:devtools_app_shared/service.dart' ;
9+ import 'package:devtools_app_shared/utils.dart' ;
10+ import 'package:flutter/foundation.dart' ;
11+ import 'package:vm_service/vm_service.dart' ;
12+
13+ import '../../service/vm_service_wrapper.dart' ;
514import '../../shared/analytics/metrics.dart' ;
615import '../../shared/console/primitives/simple_items.dart' ;
16+ import '../../shared/diagnostics/diagnostics_node.dart' ;
717import '../../shared/framework/screen.dart' ;
818import '../../shared/framework/screen_controllers.dart' ;
19+ import '../../shared/globals.dart' ;
20+ import '../../shared/managers/error_badge_manager.dart' ;
21+ import '../../shared/primitives/query_parameters.dart' ;
922import 'inspector_controller.dart' ;
23+ import 'inspector_errors.dart' ;
24+ import 'inspector_screen.dart' ;
1025import 'inspector_tree_controller.dart' ;
1126
1227/// Screen controller for the Inspector screen.
@@ -19,27 +34,164 @@ import 'inspector_tree_controller.dart';
1934/// `init` method is called lazily upon the first controller access from
2035/// `screenControllers` . The `dispose` method is called by `screenControllers`
2136/// when DevTools is destroying a set of DevTools screen controllers.
22- class InspectorScreenController extends DevToolsScreenController {
37+ class InspectorScreenController extends DevToolsScreenController
38+ with AutoDisposeControllerMixin {
2339 @override
2440 final screenId = ScreenMetaData .inspector.id;
2541
2642 late InspectorController inspectorController;
2743 late InspectorTreeController inspectorTreeController;
2844
45+ /// Stores the inspector-specific errors keyed by inspector reference ID.
46+ final _activeInspectorErrors =
47+ ValueNotifier <LinkedHashMap <String , DevToolsError >>(
48+ LinkedHashMap <String , DevToolsError >(),
49+ );
50+
51+ /// The errors currently tracked for the inspector screen.
52+ ValueListenable <LinkedHashMap <String , DevToolsError >> get inspectorErrors =>
53+ _activeInspectorErrors;
54+
55+ /// The count of unread inspector errors (used for the badge).
56+ ValueListenable <int > get inspectorErrorCount => serviceConnection
57+ .errorBadgeManager
58+ .errorCountNotifier (InspectorScreen .id);
59+
2960 @override
3061 void init () {
3162 super .init ();
63+ // Inspector owns unread state for its badge; scaffold tab switches must not
64+ // clear it. See https://github.com/flutter/devtools/pull/9805.
65+ serviceConnection.errorBadgeManager.manageErrorCount (InspectorScreen .id);
66+
3267 inspectorTreeController = InspectorTreeController (
3368 gaId: InspectorScreenMetrics .summaryTreeGaId,
3469 );
3570 inspectorController = InspectorController (
3671 inspectorTree: inspectorTreeController,
3772 treeType: FlutterTreeType .widget,
3873 );
74+
75+ // Listen for Flutter extension events to extract inspector-specific errors.
76+ // Match other screen controllers: attach now if connected, and on connect.
77+ addAutoDisposeListener (serviceConnection.serviceManager.connectedState, () {
78+ if (serviceConnection.serviceManager.connectedState.value.connected) {
79+ _handleConnectionStart (serviceConnection.serviceManager.service! );
80+ }
81+ });
82+ if (serviceConnection.serviceManager.connectedAppInitialized) {
83+ _handleConnectionStart (serviceConnection.serviceManager.service! );
84+ }
85+ }
86+
87+ void _handleConnectionStart (VmServiceWrapper service) {
88+ autoDisposeStreamSubscription (
89+ service.onExtensionEventWithHistorySafe.listen (_handleExtensionEvent),
90+ );
91+ }
92+
93+ void _handleExtensionEvent (Event e) {
94+ if (e.extensionKind == FlutterEvent .error) {
95+ final inspectableError = _extractInspectableError (e);
96+ if (inspectableError != null ) {
97+ appendError (inspectableError);
98+ }
99+ }
100+ }
101+
102+ InspectableWidgetError ? _extractInspectableError (Event error) {
103+ final extensionData = error.extensionData;
104+ if (extensionData == null ) return null ;
105+
106+ final node = RemoteDiagnosticsNode (extensionData.data, null , false , null );
107+
108+ final errorSummaryNode = node.inlineProperties.firstWhereOrNull (
109+ (p) => p.type == 'ErrorSummary' ,
110+ );
111+ final errorMessage = errorSummaryNode? .description;
112+ if (errorMessage == null ) {
113+ return null ;
114+ }
115+
116+ final devToolsUrlNode = node.inlineProperties.firstWhereOrNull (
117+ (p) =>
118+ p.type == 'DevToolsDeepLinkProperty' &&
119+ p.getStringMember ('value' ) != null ,
120+ );
121+ if (devToolsUrlNode == null ) {
122+ return null ;
123+ }
124+
125+ final queryParams = DevToolsQueryParams .fromUrl (
126+ devToolsUrlNode.getStringMember ('value' )! ,
127+ );
128+ final inspectorRef = queryParams.inspectorRef ?? '' ;
129+
130+ return InspectableWidgetError (errorMessage, inspectorRef);
131+ }
132+
133+ /// Appends an error to the inspector's active errors and updates the badge
134+ /// count.
135+ void appendError (DevToolsError error) {
136+ final errors = _activeInspectorErrors;
137+ final previousError = errors.value[error.id];
138+
139+ // Build a new map with the new error. Adding to the existing map
140+ // won't cause the ValueNotifier to fire (and it's not permitted to call
141+ // notifyListeners() directly).
142+ final newValue = LinkedHashMap <String , DevToolsError >.of (errors.value);
143+ newValue[error.id] = error;
144+ errors.value = newValue;
145+
146+ if (previousError == null ) {
147+ if (! error.read) {
148+ _incrementUnreadCount ();
149+ }
150+ return ;
151+ }
152+
153+ if (previousError.read && ! error.read) {
154+ _incrementUnreadCount ();
155+ } else if (! previousError.read && error.read) {
156+ _decrementUnreadCount ();
157+ }
158+ }
159+
160+ /// Clears all inspector errors and resets the badge count.
161+ void clearErrors () {
162+ _activeInspectorErrors.value = LinkedHashMap <String , DevToolsError >();
163+ serviceConnection.errorBadgeManager.resetErrorCount (InspectorScreen .id);
164+ }
165+
166+ /// Marks an error as read and decrements the unread count.
167+ void markErrorAsRead (DevToolsError error) {
168+ final errors = _activeInspectorErrors;
169+
170+ // If this error doesn't exist anymore or is already read, nothing to do.
171+ final currentError = errors.value[error.id];
172+ if (currentError == null || currentError.read) {
173+ return ;
174+ }
175+
176+ // Otherwise, replace the map with a new one that has the error marked
177+ // as read.
178+ final newValue = LinkedHashMap <String , DevToolsError >.of (errors.value);
179+ newValue[error.id] = currentError.asRead ();
180+ errors.value = newValue;
181+ _decrementUnreadCount ();
182+ }
183+
184+ void _incrementUnreadCount () {
185+ serviceConnection.errorBadgeManager.incrementBadgeCount (InspectorScreen .id);
186+ }
187+
188+ void _decrementUnreadCount () {
189+ serviceConnection.errorBadgeManager.decrementBadgeCount (InspectorScreen .id);
39190 }
40191
41192 @override
42193 void dispose () {
194+ _activeInspectorErrors.dispose ();
43195 inspectorTreeController.dispose ();
44196 inspectorController.dispose ();
45197 super .dispose ();
0 commit comments