您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
1368 行
46 KiB
1368 行
46 KiB
/*using System;
|
|
using System.Collections.Generic;
|
|
using Unity.UIWidgets.async2;
|
|
using Unity.UIWidgets.DevTools.inspector;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.rendering;
|
|
using UnityEngine;
|
|
using Object = System.Object;
|
|
|
|
namespace Unity.UIWidgets.DevTools.inspector
|
|
{
|
|
public class inspector_service
|
|
{
|
|
public static readonly List<string> inspectorLibraryUriCandidates = new List<string>{
|
|
"package:flutter/src/widgets/widget_inspector.dart",
|
|
"package:flutter_web/src/widgets/widget_inspector.dart",
|
|
};
|
|
|
|
bool _inspectorDependenciesLoaded = false;
|
|
|
|
Future ensureInspectorServiceDependencies() {
|
|
if (_inspectorDependenciesLoaded) {
|
|
return Future.value();
|
|
}
|
|
// TODO(jacobr): consider also loading common icons needed by the inspector
|
|
// to avoid flicker on icon load.
|
|
_inspectorDependenciesLoaded = true;
|
|
return Future.value();
|
|
}
|
|
}
|
|
|
|
|
|
public class RegistrableServiceExtension {
|
|
public RegistrableServiceExtension(string name)
|
|
{
|
|
this.name = name;
|
|
}
|
|
|
|
public readonly string name;
|
|
|
|
public static readonly RegistrableServiceExtension getLayoutExplorerNode =
|
|
new RegistrableServiceExtension("getLayoutExplorerNode");
|
|
public static readonly RegistrableServiceExtension setFlexFit = new RegistrableServiceExtension("setFlexFit");
|
|
public static readonly RegistrableServiceExtension setFlexFactor = new RegistrableServiceExtension("setFlexFactor");
|
|
public static readonly RegistrableServiceExtension setFlexProperties =
|
|
new RegistrableServiceExtension("setFlexProperties");
|
|
|
|
public static readonly RegistrableServiceExtension getPubRootDirectories =
|
|
new RegistrableServiceExtension("getPubRootDirectories");
|
|
}
|
|
|
|
|
|
public class InspectorService : DisposableController, AutoDisposeControllerMixin {
|
|
public InspectorService(
|
|
VmService vmService,
|
|
HashSet<InspectorServiceClient> inspectorLibrary,
|
|
HashSet<string> supportedServiceMethods
|
|
)
|
|
{
|
|
this.vmService = vmService;
|
|
this.inspectorLibrary = inspectorLibrary;
|
|
this.supportedServiceMethods = supportedServiceMethods;
|
|
|
|
clients = new HashSet<InspectorServiceClient>();
|
|
// Note: We do not need to listen to event history here because the
|
|
// inspector uses a separate API to get the current inspector selection.
|
|
autoDispose(
|
|
vmService.onExtensionEvent.listen(onExtensionVmServiceRecieved));
|
|
autoDispose(vmService.onDebugEvent.listen(onDebugVmServiceReceived));
|
|
|
|
autoDispose(serviceManager.isolateManager
|
|
.getSelectedIsolate((IsolateRef flutterIsolate) => {
|
|
// Any time we have a new isolate it means the previous isolate stopped.
|
|
_onIsolateStopped();
|
|
}));
|
|
}
|
|
|
|
static int nextGroupId = 0;
|
|
|
|
public readonly VmService vmService;
|
|
public readonly HashSet<InspectorServiceClient> clients;
|
|
public readonly EvalOnDartLibrary inspectorLibrary;
|
|
public readonly HashSet<string> supportedServiceMethods;
|
|
|
|
/// [ensureInspectorServiceDependencies] must be called before this method is
|
|
/// called.
|
|
static Future<ObjectGroup> createGroup(
|
|
VmService vmService,
|
|
String groupName,
|
|
) async {
|
|
assert(_inspectorDependenciesLoaded);
|
|
return (await create(vmService)).createObjectGroup(groupName);
|
|
}
|
|
|
|
static Future<InspectorService> create(VmService vmService) async {
|
|
assert(_inspectorDependenciesLoaded);
|
|
assert(serviceManager.hasConnection);
|
|
assert(serviceManager.service != null);
|
|
final inspectorLibrary = EvalOnDartLibrary(
|
|
inspectorLibraryUriCandidates,
|
|
vmService,
|
|
);
|
|
|
|
final libraryRef = await inspectorLibrary.libraryRef.catchError(
|
|
(_) => throw FlutterInspectorLibraryNotFound(),
|
|
test: (e) => e is LibraryNotFound,
|
|
);
|
|
final libraryFuture = inspectorLibrary.getLibrary(libraryRef, null);
|
|
final library = await libraryFuture;
|
|
Future<Set<String>> lookupFunctionNames() async {
|
|
for (ClassRef classRef in library.classes) {
|
|
if ('WidgetInspectorService' == classRef.name) {
|
|
final classObj = await inspectorLibrary.getClass(classRef, null);
|
|
final functionNames = <String>{};
|
|
for (FuncRef funcRef in classObj.functions) {
|
|
functionNames.add(funcRef.name);
|
|
}
|
|
return functionNames;
|
|
}
|
|
}
|
|
// WidgetInspectorService is not available. Either this is not a Flutter
|
|
// application or it is running in profile mode.
|
|
return null;
|
|
}
|
|
|
|
final supportedServiceMethods = await lookupFunctionNames();
|
|
if (supportedServiceMethods == null) return null;
|
|
return InspectorService(
|
|
vmService,
|
|
inspectorLibrary,
|
|
supportedServiceMethods,
|
|
);
|
|
}
|
|
|
|
void _onIsolateStopped() {
|
|
// Clear data that is obsolete on an isolate restart.
|
|
_currentSelection = null;
|
|
_cachedSelectionGroups?.clear(true);
|
|
_expectedSelectionChanges.clear();
|
|
}
|
|
|
|
/// Map from InspectorInstanceRef to list of timestamps when a selection
|
|
/// change to that ref was triggered by this application.
|
|
///
|
|
/// This is needed to handle the case where we may send multiple selection
|
|
/// change notifications to the device before we get a notification back that
|
|
/// the selection has actually changed. Without this fix it was rare but
|
|
/// possible to trigger an infinite loop ping-ponging back and forth between
|
|
/// selecting two different nodes in the inspector tree if the selection was
|
|
/// changed more rapidly than the running flutter app could update.
|
|
final Map<InspectorInstanceRef, List<int>> _expectedSelectionChanges = {};
|
|
|
|
/// Maximum time in milliseconds that we ever expect it will take for a
|
|
/// selection change to apply.
|
|
///
|
|
/// In general this heuristic based time should not matter but we keep it
|
|
/// anyway so that in the unlikely event that package:flutter changes and we
|
|
/// do not received all of the selection notification events we expect, we
|
|
/// will not be impacted if there is at least the following delay between
|
|
/// when selection was set to exactly the same location by both the on device
|
|
/// inspector and DevTools.
|
|
static const _maxTimeDelaySelectionNotification = 5000;
|
|
|
|
void _trackClientSelfTriggeredSelection(InspectorInstanceRef ref) {
|
|
_expectedSelectionChanges
|
|
.putIfAbsent(ref, () => [])
|
|
.add(DateTime.now().millisecondsSinceEpoch);
|
|
}
|
|
|
|
/// Returns whether the selection change was originally triggered by this
|
|
/// application.
|
|
///
|
|
/// This method is needed to avoid a race condition when there is a queue of
|
|
/// inspector selection changes due to extremely rapidly navigating through
|
|
/// the inspector tree such as when using the keyboard to navigate.
|
|
bool _isClientTriggeredSelectionChange(InspectorInstanceRef ref) {
|
|
// TODO(jacobr): once https://github.com/flutter/flutter/issues/39366 is
|
|
// fixed in all versions of flutter we support, remove this logic and
|
|
// determine the source of the inspector selection change directly from the
|
|
// inspector selection changed event.
|
|
final currentTime = DateTime.now().millisecondsSinceEpoch;
|
|
if (ref != null) {
|
|
if (_expectedSelectionChanges.containsKey(ref)) {
|
|
final times = _expectedSelectionChanges.remove(ref);
|
|
while (times.isNotEmpty) {
|
|
final time = times.removeAt(0);
|
|
if (time + _maxTimeDelaySelectionNotification >= currentTime) {
|
|
// We triggered this selection change ourselves. This logic would
|
|
// work fine without the timestamps for the typical case but we use
|
|
// the timestamps to be safe in case there is a bug and selection
|
|
// change events were somehow lost.
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
ValueListenable<List<String>> get rootDirectories => _rootDirectories;
|
|
final ValueNotifier<List<String>> _rootDirectories = ValueNotifier([]);
|
|
|
|
@visibleForTesting
|
|
Set<String> get rootPackages => _rootPackages;
|
|
Set<String> _rootPackages;
|
|
|
|
@visibleForTesting
|
|
List<String> get rootPackagePrefixes => _rootPackagePrefixes;
|
|
List<String> _rootPackagePrefixes;
|
|
|
|
Future<void> _onRootDirectoriesChanged(List<String> directories) async {
|
|
_rootDirectories.value = directories;
|
|
_rootPackages = {};
|
|
_rootPackagePrefixes = [];
|
|
for (var directory in directories) {
|
|
// TODO(jacobr): add an API to DDS to provide the actual mapping to and
|
|
// from absolute file paths to packages instead of having to guess it
|
|
// here.
|
|
assert(!directory.startsWith('package:'));
|
|
|
|
final parts =
|
|
directory.split('/').where((element) => element.isNotEmpty).toList();
|
|
final libIndex = parts.lastIndexOf('lib');
|
|
final path = libIndex > 0 ? parts.sublist(0, libIndex) : parts;
|
|
// Special case handling of bazel packages.
|
|
final google3Index = path.lastIndexOf('google3');
|
|
if (google3Index != -1 && google3Index + 1 < path.length) {
|
|
var packageParts = path.sublist(google3Index + 1);
|
|
// A well formed third_party dart package should be in a directory of
|
|
// the form
|
|
// third_party/dart/packageName (package:packageName)
|
|
// or
|
|
// third_party/dart_src/long/package/name (package:long.package.name)
|
|
// so its path should be at minimum depth 3.
|
|
const minThirdPartyPathDepth = 3;
|
|
if (packageParts[0] == 'third_party' &&
|
|
packageParts.length >= minThirdPartyPathDepth) {
|
|
assert(packageParts[1] == 'dart' || packageParts[1] == 'dart_src');
|
|
packageParts = packageParts.sublist(2);
|
|
}
|
|
final google3PackageName = packageParts.join('.');
|
|
_rootPackages.add(google3PackageName);
|
|
_rootPackagePrefixes.add(google3PackageName + '.');
|
|
} else {
|
|
_rootPackages.add(path.last);
|
|
}
|
|
}
|
|
|
|
await _updateLocalClasses();
|
|
}
|
|
|
|
Future<void> _updateLocalClasses() async {
|
|
localClasses.clear();
|
|
if (_rootDirectories.value.isNotEmpty) {
|
|
final isolate = inspectorLibrary.isolate;
|
|
for (var libraryRef in isolate.libraries) {
|
|
if (isLocalUri(libraryRef.uri)) {
|
|
final Library library = await inspectorLibrary.service
|
|
.getObject(isolate.id, libraryRef.id);
|
|
for (var classRef in library.classes) {
|
|
localClasses[classRef.name] = classRef;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
bool isLocalUri(String rawUri) {
|
|
final uri = Uri.parse(rawUri);
|
|
if (uri.scheme != 'file' && uri.scheme != 'dart') {
|
|
// package scheme or some other dart specific scheme.
|
|
final packageName = uri.pathSegments.first;
|
|
if (_rootPackages.contains(packageName)) return true;
|
|
|
|
// This attempts to gracefully handle the bazel package case.
|
|
return _rootPackagePrefixes
|
|
.any((prefix) => packageName.startsWith(prefix));
|
|
}
|
|
for (var root in _rootDirectories.value) {
|
|
if (root.endsWith(rawUri)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@visibleForTesting
|
|
final Map<String, ClassRef> localClasses = {};
|
|
|
|
bool isLocalClass(RemoteDiagnosticsNode node) {
|
|
if (node.widgetRuntimeType == null) return false;
|
|
// widgetRuntimeType may contain some generic type arguments which we need
|
|
// to strip out. If widgetRuntimeType is "FooWidget<Bar>" then we are only
|
|
// interested in the raw type "FooWidget".
|
|
final rawType = node.widgetRuntimeType.split('<').first;
|
|
return localClasses.containsKey(rawType);
|
|
}
|
|
|
|
/// As we aren't running from an IDE, we don't know exactly what the pub root
|
|
/// directories are for the current project so we make a best guess if needed
|
|
/// based on the the root directory of the first non artifical widget in the
|
|
/// tree.
|
|
Future<List<String>> inferPubRootDirectoryIfNeeded() async {
|
|
final group = createObjectGroup('temp');
|
|
List<String> directories = await group.getPubRootDirectories() ?? [];
|
|
if (directories.isEmpty) {
|
|
final directory = await inferPubRootDirectoryIfNeededHelper();
|
|
if (directory != null) {
|
|
directories = [directory];
|
|
}
|
|
}
|
|
|
|
await _onRootDirectoriesChanged(directories);
|
|
return directories;
|
|
}
|
|
|
|
Future<String> inferPubRootDirectoryIfNeededHelper() async {
|
|
final group = createObjectGroup('temp');
|
|
final root = await group.getRoot(FlutterTreeType.widget);
|
|
|
|
if (root == null) {
|
|
// No need to do anything as there isn't a valid tree (yet?).
|
|
await group.dispose();
|
|
return null;
|
|
}
|
|
List<RemoteDiagnosticsNode> children = await root.children;
|
|
|
|
if (children?.isEmpty ?? true) {
|
|
children = await group.getChildren(root.dartDiagnosticRef, false, null);
|
|
}
|
|
|
|
if (children?.isEmpty ?? true) {
|
|
await group.dispose();
|
|
return null;
|
|
}
|
|
final path = children.first.creationLocation?.path;
|
|
if (path == null) {
|
|
await group.dispose();
|
|
return null;
|
|
}
|
|
// TODO(jacobr): it would be nice to use Isolate.rootLib similar to how
|
|
// debugger.dart does but we are currently blocked by the
|
|
// --track-widget-creation transformer generating absolute paths instead of
|
|
// package:paths.
|
|
// Once https://github.com/flutter/flutter/issues/26615 is fixed we will be
|
|
// able to use package: paths. Temporarily all tools tracking widget
|
|
// locations will need to support both path formats.
|
|
// TODO(jacobr): use the list of loaded scripts to determine the appropriate
|
|
// package root directory given that the root script of this project is in
|
|
// this directory rather than guessing based on url structure.
|
|
final parts = path.split('/');
|
|
String pubRootDirectory;
|
|
for (int i = parts.length - 1; i >= 0; i--) {
|
|
final part = parts[i];
|
|
if (part == 'lib' || part == 'web') {
|
|
pubRootDirectory = parts.sublist(0, i).join('/');
|
|
break;
|
|
}
|
|
|
|
if (part == 'packages') {
|
|
pubRootDirectory = parts.sublist(0, i + 1).join('/');
|
|
break;
|
|
}
|
|
}
|
|
pubRootDirectory ??= (parts..removeLast()).join('/');
|
|
|
|
await _setPubRootDirectories([pubRootDirectory]);
|
|
await group.dispose();
|
|
return pubRootDirectory;
|
|
}
|
|
|
|
/// Returns whether to use the Daemon API or the VM Service protocol directly.
|
|
///
|
|
/// The VM Service protocol must be used when paused at a breakpoint as the
|
|
/// Daemon API calls won't execute until after the current frame is done
|
|
/// rendering.
|
|
bool get useDaemonApi {
|
|
return true;
|
|
// TODO(jacobr): once there is a debugger, hook to it to determine whether
|
|
// we are suspended.
|
|
// return !app.isFlutterIsolateSuspended();
|
|
}
|
|
|
|
/// Use this method to write code that is backwards compatible with versions
|
|
/// of Flutter that are too old to contain specific service methods.
|
|
bool hasServiceMethod(String methodName) {
|
|
return supportedServiceMethods.contains(methodName);
|
|
}
|
|
|
|
ObjectGroup createObjectGroup(String debugName) {
|
|
return ObjectGroup(debugName, this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
inspectorLibrary.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<Object> forceRefresh() {
|
|
final List<Future<Object>> futures = [];
|
|
for (InspectorServiceClient client in clients) {
|
|
try {
|
|
futures.add(client.onForceRefresh());
|
|
} catch (e) {
|
|
log(e);
|
|
}
|
|
}
|
|
return Future.wait(futures);
|
|
}
|
|
|
|
RemoteDiagnosticsNode _currentSelection;
|
|
|
|
InspectorObjectGroupManager get _selectionGroups {
|
|
return _cachedSelectionGroups ??=
|
|
InspectorObjectGroupManager(this, 'selection');
|
|
}
|
|
|
|
InspectorObjectGroupManager _cachedSelectionGroups;
|
|
|
|
void notifySelectionChanged() async {
|
|
// The previous selection changed event is obsolete.
|
|
_selectionGroups.cancelNext();
|
|
final group = _selectionGroups.next;
|
|
final pendingSelection = await group.getSelection(
|
|
_currentSelection,
|
|
FlutterTreeType.widget,
|
|
isSummaryTree: false,
|
|
);
|
|
if (!group.disposed &&
|
|
group == _selectionGroups.next &&
|
|
!_isClientTriggeredSelectionChange(pendingSelection?.valueRef)) {
|
|
_currentSelection = pendingSelection;
|
|
assert(group == _selectionGroups.next);
|
|
_selectionGroups.promoteNext();
|
|
for (InspectorServiceClient client in clients) {
|
|
client.onInspectorSelectionChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
void addClient(InspectorServiceClient client) {
|
|
clients.add(client);
|
|
}
|
|
|
|
void onDebugVmServiceReceived(Event event) {
|
|
if (event.kind == EventKind.kInspect) {
|
|
// Update the UI in IntelliJ.
|
|
notifySelectionChanged();
|
|
}
|
|
}
|
|
|
|
void onExtensionVmServiceRecieved(Event e) {
|
|
if ('Flutter.Frame' == e.extensionKind) {
|
|
for (InspectorServiceClient client in clients) {
|
|
try {
|
|
client.onFlutterFrame();
|
|
} catch (e) {
|
|
log('Error handling frame event', error: e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If the widget tree is not ready, the application should wait for the next
|
|
/// Flutter.Frame event before attempting to display the widget tree. If the
|
|
/// application is ready, the next Flutter.Frame event may never come as no
|
|
/// new frames will be triggered to draw unless something changes in the UI.
|
|
Future<bool> isWidgetTreeReady() {
|
|
return invokeBoolServiceMethodNoArgs('isWidgetTreeReady');
|
|
}
|
|
|
|
Future<bool> invokeBoolServiceMethodNoArgs(String methodName) async {
|
|
if (useDaemonApi) {
|
|
return await invokeServiceMethodDaemonNoGroupArgs(methodName) == true;
|
|
} else {
|
|
return (await invokeServiceMethodObservatoryNoGroup(methodName))
|
|
?.valueAsString ==
|
|
'true';
|
|
}
|
|
}
|
|
|
|
Future<bool> isWidgetCreationTracked() {
|
|
return invokeBoolServiceMethodNoArgs('isWidgetCreationTracked');
|
|
}
|
|
|
|
Future<Object> invokeServiceMethodDaemonNoGroupArgs(String methodName,
|
|
[List<String> args]) {
|
|
final Map<String, Object> params = {};
|
|
if (args != null) {
|
|
for (int i = 0; i < args.length; ++i) {
|
|
params['arg$i'] = args[i];
|
|
}
|
|
}
|
|
return invokeServiceMethodDaemonNoGroup(methodName, params);
|
|
}
|
|
|
|
Future<void> setPubRootDirectories(List<String> rootDirectories) async {
|
|
await _setPubRootDirectories(rootDirectories);
|
|
await _onRootDirectoriesChanged(rootDirectories);
|
|
}
|
|
|
|
Future<void> _setPubRootDirectories(List<String> rootDirectories) {
|
|
// No need to call this from a breakpoint.
|
|
assert(useDaemonApi);
|
|
return invokeServiceMethodDaemonNoGroupArgs(
|
|
'setPubRootDirectories',
|
|
rootDirectories,
|
|
);
|
|
}
|
|
|
|
Future<List<String>> getPubRootDirectories() {
|
|
// No need to call this from a breakpoint.
|
|
assert(useDaemonApi);
|
|
final result =
|
|
invokeServiceMethodDaemonNoGroup('getPubRootDirectories', null);
|
|
return result ?? [];
|
|
}
|
|
|
|
Future<InstanceRef> invokeServiceMethodObservatoryNoGroup(String methodName) {
|
|
return inspectorLibrary
|
|
.eval('WidgetInspectorService.instance.$methodName()', isAlive: null);
|
|
}
|
|
|
|
Future<Object> invokeServiceMethodDaemonNoGroup(
|
|
String methodName, Map<String, Object> args) async {
|
|
final callMethodName = 'ext.flutter.inspector.$methodName';
|
|
if (!serviceManager.serviceExtensionManager
|
|
.isServiceExtensionAvailable(callMethodName)) {
|
|
final available = await serviceManager.serviceExtensionManager
|
|
.waitForServiceExtensionAvailable(callMethodName);
|
|
if (!available) return {'result': null};
|
|
}
|
|
|
|
final r = await vmService.callServiceExtension(
|
|
callMethodName,
|
|
isolateId: inspectorLibrary.isolateId,
|
|
args: args,
|
|
);
|
|
final json = r.json;
|
|
if (json['errorMessage'] != null) {
|
|
throw Exception('$methodName -- ${json['errorMessage']}');
|
|
}
|
|
return json['result'];
|
|
}
|
|
|
|
void removeClient(InspectorServiceClient client) {
|
|
clients.remove(client);
|
|
}
|
|
}
|
|
|
|
/// Class managing a group of inspector objects that can be freed by
|
|
/// a single call to dispose().
|
|
/// After dispose is called, all pending requests made with the ObjectGroup
|
|
/// will be skipped. This means that clients should not have to write any
|
|
/// special logic to handle orphaned requests.
|
|
public class ObjectGroup {
|
|
public ObjectGroup(
|
|
string debugName,
|
|
this.inspectorService,
|
|
) : groupName = '${debugName}_${InspectorService.nextGroupId}' {
|
|
InspectorService.nextGroupId++;
|
|
}
|
|
|
|
/// Object group all objects in this arena are allocated with.
|
|
final String groupName;
|
|
final InspectorService inspectorService;
|
|
bool disposed = false;
|
|
|
|
EvalOnDartLibrary get inspectorLibrary => inspectorService.inspectorLibrary;
|
|
|
|
bool get useDaemonApi => inspectorService.useDaemonApi;
|
|
|
|
/// Once an ObjectGroup has been disposed, all methods returning
|
|
/// DiagnosticsNode objects will return a placeholder dummy node and all methods
|
|
/// returning lists or maps will return empty lists and all other methods will
|
|
/// return null. Generally code should never call methods on a disposed object
|
|
/// group but sometimes due to chained futures that can be difficult to avoid
|
|
/// and it is simpler return an empty result that will be ignored anyway than to
|
|
/// attempt carefully cancel futures.
|
|
Future dispose() {
|
|
var disposeComplete = invokeVoidServiceMethod("disposeGroup", groupName);
|
|
disposed = true;
|
|
return disposeComplete;
|
|
}
|
|
|
|
Future<T> nullIfDisposed<T>(Future<T> supplier()) {
|
|
if (disposed) {
|
|
return null;
|
|
}
|
|
return await supplier();
|
|
}
|
|
|
|
T nullValueIfDisposed<T>(T supplier()) {
|
|
if (disposed) {
|
|
return null;
|
|
}
|
|
|
|
return supplier();
|
|
}
|
|
|
|
void skipIfDisposed(void runnable()) {
|
|
if (disposed) {
|
|
return;
|
|
}
|
|
|
|
runnable();
|
|
}
|
|
|
|
Future<SourcePosition> getPropertyLocation(
|
|
InstanceRef instanceRef, string name) async {
|
|
Instance instance = await getInstance(instanceRef);
|
|
if (instance == null || disposed) {
|
|
return null;
|
|
}
|
|
return getPropertyLocationHelper(instance.classRef, name);
|
|
}
|
|
|
|
Future<SourcePosition> getPropertyLocationHelper(
|
|
ClassRef classRef, String name) async {
|
|
var clazz = await inspectorLibrary.getClass(classRef, this);
|
|
foreach (FuncRef f in clazz.functions) {
|
|
// TODO(pq): check for properties that match name.
|
|
if (f.name == name) {
|
|
var func = await inspectorLibrary.getFunc(f, this);
|
|
var SourceLocation location = func.location;
|
|
throw UnimplementedError(
|
|
$"getSourcePosition not implemented. {location}");
|
|
// return inspectorLibrary.getSourcePosition(
|
|
// debugProcess, location.script, location.tokenPos, this);
|
|
}
|
|
}
|
|
ClassRef superClass = clazz.superClass;
|
|
return superClass == null
|
|
? null
|
|
: getPropertyLocationHelper(superClass, name);
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getRoot(FlutterTreeType type) {
|
|
// There is no excuse to call this method on a disposed group.
|
|
D.assert(!disposed);
|
|
switch (type) {
|
|
case FlutterTreeType.widget:
|
|
return getRootWidget();
|
|
case FlutterTreeType.renderObject:
|
|
return getRootRenderObject();
|
|
}
|
|
throw new Exception("Unexpected FlutterTreeType");
|
|
}
|
|
|
|
/// Invokes a static method on the WidgetInspectorService class passing in the specified
|
|
/// arguments.
|
|
///
|
|
/// Intent is we could refactor how the API is invoked by only changing this call.
|
|
Future<InstanceRef> invokeServiceMethodObservatory(string methodName) {
|
|
return invokeServiceMethodObservatory1(methodName, groupName);
|
|
}
|
|
|
|
Future<InstanceRef> invokeServiceMethodObservatory1(
|
|
string methodName, string arg1) {
|
|
return inspectorLibrary.eval(
|
|
"WidgetInspectorService.instance.$methodName('$arg1')",
|
|
isAlive: this
|
|
);
|
|
}
|
|
|
|
Future<object> invokeServiceExtensionMethod(
|
|
RegistrableServiceExtension extension,
|
|
Dictionary<string, string> parameters
|
|
) async {
|
|
var name = extension.name;
|
|
var fullName = "ext.flutter.inspector.$name";
|
|
if (!serviceManager.serviceExtensionManager
|
|
.isServiceExtensionAvailable(fullName)) {
|
|
// Wait until a service extension we know will be eventually available for
|
|
// a Flutter app is loaded to avoid attempting to apply the polyfill
|
|
// while the list of Flutter service extensions is really just being
|
|
// registered on the device. This prevents pew in the app console about
|
|
// trying to register service extensions multiple times.
|
|
final regularExtensionsRegistered = await serviceManager
|
|
.serviceExtensionManager
|
|
.waitForServiceExtensionAvailable(
|
|
'ext.flutter.inspector.isWidgetCreationTracked');
|
|
if (disposed) return null;
|
|
assert(regularExtensionsRegistered);
|
|
if (!serviceManager.serviceExtensionManager
|
|
.isServiceExtensionAvailable(fullName)) {
|
|
await invokeInspectorPolyfill(this);
|
|
}
|
|
if (disposed) return null;
|
|
}
|
|
return invokeServiceMethodDaemonParams(name, parameters);
|
|
}
|
|
|
|
Future<object> invokeServiceMethodDaemon(string methodName,
|
|
string objectGroup = null) {
|
|
return invokeServiceMethodDaemonParams(
|
|
methodName,
|
|
"objectGroup": objectGroup ?? groupName
|
|
);
|
|
}
|
|
|
|
Future<Object> invokeServiceMethodDaemonArg(
|
|
String methodName, String arg, String objectGroup) {
|
|
final args = {'objectGroup': objectGroup};
|
|
if (arg != null) {
|
|
args['arg'] = arg;
|
|
}
|
|
return invokeServiceMethodDaemonParams(methodName, args);
|
|
}
|
|
|
|
Future<Object> _callServiceExtension(
|
|
String extension, Map<String, Object> args) {
|
|
if (disposed) {
|
|
return Future.value();
|
|
}
|
|
|
|
return inspectorLibrary.addRequest(this, () async {
|
|
final r = await inspectorService.vmService.callServiceExtension(
|
|
extension,
|
|
isolateId: inspectorService.inspectorLibrary.isolateId,
|
|
args: args,
|
|
);
|
|
if (disposed) return null;
|
|
final json = r.json;
|
|
if (json['errorMessage'] != null) {
|
|
throw Exception('$extension -- ${json['errorMessage']}');
|
|
}
|
|
return json['result'];
|
|
});
|
|
}
|
|
|
|
// All calls to invokeServiceMethodDaemon bottom out to this call.
|
|
Future<object> invokeServiceMethodDaemonParams(
|
|
string methodName,
|
|
Dictionary<string, object> _params
|
|
) async {
|
|
var callMethodName = $"ext.flutter.inspector.{methodName}";
|
|
if (!serviceManager.serviceExtensionManager
|
|
.isServiceExtensionAvailable(callMethodName)) {
|
|
final available = await serviceManager.serviceExtensionManager
|
|
.waitForServiceExtensionAvailable(callMethodName);
|
|
if (!available) return null;
|
|
}
|
|
|
|
return await _callServiceExtension(callMethodName, params);
|
|
}
|
|
|
|
Future<Object> invokeServiceMethodDaemonInspectorRef(
|
|
String methodName, InspectorInstanceRef arg) {
|
|
return invokeServiceMethodDaemonArg(methodName, arg?.id, groupName);
|
|
}
|
|
|
|
Future<InstanceRef> invokeServiceMethodObservatoryInspectorRef(
|
|
String methodName, InspectorInstanceRef arg) {
|
|
return inspectorLibrary.eval(
|
|
"WidgetInspectorService.instance.$methodName('${arg?.id}', '$groupName')",
|
|
isAlive: this);
|
|
}
|
|
|
|
/// Call a service method passing in an observatory instance reference.
|
|
///
|
|
/// This call is useful when receiving an 'inspect' event from the
|
|
/// observatory and future use cases such as inspecting a Widget from a
|
|
/// log window.
|
|
///
|
|
/// This method will always need to use the observatory service as the input
|
|
/// parameter is an Observatory InstanceRef..
|
|
Future<InstanceRef> invokeServiceMethodOnRefObservatory(
|
|
String methodName, InstanceRef arg) {
|
|
if (arg == null) {
|
|
return inspectorLibrary.eval(
|
|
"WidgetInspectorService.instance.$methodName(null, '$groupName')",
|
|
isAlive: this,
|
|
);
|
|
}
|
|
return inspectorLibrary.eval(
|
|
"WidgetInspectorService.instance.$methodName(arg1, '$groupName')",
|
|
isAlive: this,
|
|
scope: {'arg1': arg.id},
|
|
);
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> parseDiagnosticsNodeObservatory(
|
|
FutureOr<InstanceRef> instanceRefFuture) async {
|
|
return parseDiagnosticsNodeHelper(
|
|
await instanceRefToJson(await instanceRefFuture));
|
|
}
|
|
|
|
/// Returns a Future with a Map of property names to Observatory
|
|
/// InstanceRef objects. This method is shorthand for individually evaluating
|
|
/// each of the getters specified by property names.
|
|
///
|
|
/// It would be nice if the Observatory protocol provided a built in method
|
|
/// to get InstanceRef objects for a list of properties but this is
|
|
/// sufficient although slightly less efficient. The Observatory protocol
|
|
/// does provide fast access to all fields as part of an Instance object
|
|
/// but that is inadequate as for many Flutter data objects that we want
|
|
/// to display visually we care about properties that are not necessarily
|
|
/// fields.
|
|
///
|
|
/// The future will immediately complete to null if the inspectorInstanceRef is null.
|
|
Future<Map<String, InstanceRef>> getDartObjectProperties(
|
|
InspectorInstanceRef inspectorInstanceRef,
|
|
final List<String> propertyNames,
|
|
) async {
|
|
final instanceRef = await toObservatoryInstanceRef(inspectorInstanceRef);
|
|
if (disposed) return null;
|
|
const objectName = 'that';
|
|
final expression =
|
|
'[${propertyNames.map((propertyName) => '$objectName.$propertyName').join(',')}]';
|
|
final Map<String, String> scope = {objectName: instanceRef.id};
|
|
final instance = await getInstance(
|
|
inspectorLibrary.eval(expression, isAlive: this, scope: scope));
|
|
if (disposed) return null;
|
|
|
|
// We now have an instance object that is a Dart array of all the
|
|
// property values. Convert it back to a map from property name to
|
|
// property values.
|
|
|
|
final Map<String, InstanceRef> properties = {};
|
|
final List<InstanceRef> values = instance.elements.toList();
|
|
assert(values.length == propertyNames.length);
|
|
for (int i = 0; i < propertyNames.length; ++i) {
|
|
properties[propertyNames[i]] = values[i];
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
Future<InstanceRef> toObservatoryInstanceRef(
|
|
InspectorInstanceRef inspectorInstanceRef) {
|
|
return invokeServiceMethodObservatoryInspectorRef(
|
|
'toObject', inspectorInstanceRef);
|
|
}
|
|
|
|
Future<Instance> getInstance(FutureOr<InstanceRef> instanceRef) async {
|
|
if (disposed) {
|
|
return null;
|
|
}
|
|
return inspectorLibrary.getInstance(await instanceRef, this);
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> parseDiagnosticsNodeDaemon(
|
|
Future<Object> json) async {
|
|
if (disposed) return null;
|
|
return parseDiagnosticsNodeHelper(await json);
|
|
}
|
|
|
|
RemoteDiagnosticsNode parseDiagnosticsNodeHelper(
|
|
Map<String, Object> jsonElement) {
|
|
if (disposed) return null;
|
|
if (jsonElement == null) return null;
|
|
return RemoteDiagnosticsNode(jsonElement, this, false, null);
|
|
}
|
|
|
|
/// Requires that the InstanceRef is really referring to a String that is valid JSON.
|
|
Future<Object> instanceRefToJson(InstanceRef instanceRef) async {
|
|
if (disposed || instanceRef == null) return null;
|
|
final instance = await inspectorLibrary.getInstance(instanceRef, this);
|
|
|
|
if (disposed || instance == null) return null;
|
|
|
|
final String json = instance.valueAsString;
|
|
if (json == null) return null;
|
|
return jsonDecode(json);
|
|
}
|
|
|
|
Future<List<RemoteDiagnosticsNode>> parseDiagnosticsNodesObservatory(
|
|
FutureOr<InstanceRef> instanceRefFuture,
|
|
RemoteDiagnosticsNode parent) async {
|
|
if (disposed || instanceRefFuture == null) return [];
|
|
final instanceRef = await instanceRefFuture;
|
|
if (disposed || instanceRefFuture == null) return [];
|
|
return parseDiagnosticsNodesHelper(
|
|
await instanceRefToJson(instanceRef), parent);
|
|
}
|
|
|
|
List<RemoteDiagnosticsNode> parseDiagnosticsNodesHelper(
|
|
List<Object> jsonObject, RemoteDiagnosticsNode parent) {
|
|
if (disposed || jsonObject == null) return const [];
|
|
final List<RemoteDiagnosticsNode> nodes = [];
|
|
for (Map<String, Object> element in jsonObject) {
|
|
nodes.add(RemoteDiagnosticsNode(element, this, false, parent));
|
|
}
|
|
return nodes;
|
|
}
|
|
|
|
Future<List<RemoteDiagnosticsNode>> parseDiagnosticsNodesDaemon(
|
|
FutureOr<Object> jsonFuture, RemoteDiagnosticsNode parent) async {
|
|
if (disposed || jsonFuture == null) return const [];
|
|
|
|
return parseDiagnosticsNodesHelper(await jsonFuture, parent);
|
|
}
|
|
|
|
Future<List<RemoteDiagnosticsNode>> getChildren(
|
|
InspectorInstanceRef instanceRef,
|
|
bool summaryTree,
|
|
RemoteDiagnosticsNode parent,
|
|
) {
|
|
return getListHelper(
|
|
instanceRef,
|
|
summaryTree ? 'getChildrenSummaryTree' : 'getChildrenDetailsSubtree',
|
|
parent,
|
|
);
|
|
}
|
|
|
|
Future<List<RemoteDiagnosticsNode>> getProperties(
|
|
InspectorInstanceRef instanceRef) {
|
|
return getListHelper(instanceRef, 'getProperties', null);
|
|
}
|
|
|
|
Future<List<RemoteDiagnosticsNode>> getListHelper(
|
|
InspectorInstanceRef instanceRef,
|
|
String methodName,
|
|
RemoteDiagnosticsNode parent,
|
|
) async {
|
|
if (disposed) return const [];
|
|
if (useDaemonApi) {
|
|
return parseDiagnosticsNodesDaemon(
|
|
invokeServiceMethodDaemonInspectorRef(methodName, instanceRef),
|
|
parent);
|
|
} else {
|
|
return parseDiagnosticsNodesObservatory(
|
|
invokeServiceMethodObservatoryInspectorRef(methodName, instanceRef),
|
|
parent);
|
|
}
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> invokeServiceMethodReturningNode(
|
|
String methodName) async {
|
|
if (disposed) return null;
|
|
if (useDaemonApi) {
|
|
return parseDiagnosticsNodeDaemon(invokeServiceMethodDaemon(methodName));
|
|
} else {
|
|
return parseDiagnosticsNodeObservatory(
|
|
invokeServiceMethodObservatory(methodName));
|
|
}
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> invokeServiceMethodReturningNodeInspectorRef(
|
|
String methodName, InspectorInstanceRef ref) {
|
|
if (disposed) return null;
|
|
if (useDaemonApi) {
|
|
return parseDiagnosticsNodeDaemon(
|
|
invokeServiceMethodDaemonInspectorRef(methodName, ref));
|
|
} else {
|
|
return parseDiagnosticsNodeObservatory(
|
|
invokeServiceMethodObservatoryInspectorRef(methodName, ref));
|
|
}
|
|
}
|
|
|
|
Future<void> invokeVoidServiceMethod(String methodName, String arg1) async {
|
|
if (disposed) return;
|
|
if (useDaemonApi) {
|
|
await invokeServiceMethodDaemon(methodName, arg1);
|
|
} else {
|
|
await invokeServiceMethodObservatory1(methodName, arg1);
|
|
}
|
|
}
|
|
|
|
Future<void> invokeVoidServiceMethodInspectorRef(
|
|
String methodName, InspectorInstanceRef ref) async {
|
|
if (disposed) return;
|
|
if (useDaemonApi) {
|
|
await invokeServiceMethodDaemonInspectorRef(methodName, ref);
|
|
} else {
|
|
await invokeServiceMethodObservatoryInspectorRef(methodName, ref);
|
|
}
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getRootWidget() {
|
|
return invokeServiceMethodReturningNode('getRootWidgetSummaryTree');
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getRootWidgetFullTree() {
|
|
return invokeServiceMethodReturningNode('getRootWidget');
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getSummaryTreeWithoutIds() {
|
|
return parseDiagnosticsNodeDaemon(
|
|
invokeServiceMethodDaemon('getRootWidgetSummaryTree'));
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getRootRenderObject() {
|
|
assert(!disposed);
|
|
return invokeServiceMethodReturningNode('getRootRenderObject');
|
|
}
|
|
|
|
//// TODO(jacobr): this probably isn't needed.
|
|
////Future<List<DiagnosticsPathNode>> getParentChain(DiagnosticsNode target) async {
|
|
////if (disposed) return null;
|
|
////if (useDaemonApi) {
|
|
////return parseDiagnosticsPathDaemon(invokeServiceMethodDaemon('getParentChain', target.getValueRef()));
|
|
////}
|
|
////else {
|
|
////return parseDiagnosticsPathObservatory(invokeServiceMethodObservatory('getParentChain', target.getValueRef()));
|
|
////}
|
|
////});
|
|
////}
|
|
|
|
////Future<List<DiagnosticsPathNode>> parseDiagnosticsPathObservatory(Future<InstanceRef> instanceRefFuture) {
|
|
////return nullIfDisposed(() -> instanceRefFuture.thenComposeAsync(this::parseDiagnosticsPathObservatory));
|
|
////}
|
|
|
|
////Future<List<DiagnosticsPathNode>> parseDiagnosticsPathObservatory(InstanceRef pathRef) {
|
|
////return nullIfDisposed(() -> instanceRefToJson(pathRef).thenApplyAsync(this::parseDiagnosticsPathHelper));
|
|
////}
|
|
|
|
////Future<List<DiagnosticsPathNode>> parseDiagnosticsPathDaemon(Future<JsonElement> jsonFuture) {
|
|
////return nullIfDisposed(() -> jsonFuture.thenApplyAsync(this::parseDiagnosticsPathHelper));
|
|
////}
|
|
|
|
////List<DiagnosticsPathNode> parseDiagnosticsPathHelper(JsonElement jsonElement) {
|
|
////return nullValueIfDisposed(() -> {
|
|
////final JsonArray jsonArray = jsonElement.getAsJsonArray();
|
|
////final List<DiagnosticsPathNode> pathNodes = new List<>();
|
|
////for (JsonElement element : jsonArray) {
|
|
////pathNodes.add(new DiagnosticsPathNode(element.getAsJsonObject(), this));
|
|
////}
|
|
////return pathNodes;
|
|
////});
|
|
////}
|
|
|
|
|
|
Future<RemoteDiagnosticsNode> getSelection(
|
|
RemoteDiagnosticsNode previousSelection,
|
|
FlutterTreeType treeType, {
|
|
@required bool isSummaryTree,
|
|
}) async {
|
|
assert(isSummaryTree != null);
|
|
// There is no reason to allow calling this method on a disposed group.
|
|
assert(!disposed);
|
|
if (disposed) return null;
|
|
RemoteDiagnosticsNode newSelection;
|
|
final InspectorInstanceRef previousSelectionRef =
|
|
previousSelection != null ? previousSelection.dartDiagnosticRef : null;
|
|
|
|
switch (treeType) {
|
|
case FlutterTreeType.widget:
|
|
newSelection = await invokeServiceMethodReturningNodeInspectorRef(
|
|
isSummaryTree ? 'getSelectedSummaryWidget' : 'getSelectedWidget',
|
|
previousSelectionRef);
|
|
break;
|
|
case FlutterTreeType.renderObject:
|
|
newSelection = await invokeServiceMethodReturningNodeInspectorRef(
|
|
'getSelectedRenderObject', previousSelectionRef);
|
|
break;
|
|
}
|
|
if (disposed) return null;
|
|
|
|
if (newSelection != null &&
|
|
newSelection.dartDiagnosticRef == previousSelectionRef) {
|
|
return previousSelection;
|
|
} else {
|
|
return newSelection;
|
|
}
|
|
}
|
|
|
|
Future<void> setSelectionInspector(
|
|
InspectorInstanceRef selection, bool uiAlreadyUpdated) {
|
|
if (disposed) {
|
|
return Future.value();
|
|
}
|
|
inspectorService._trackClientSelfTriggeredSelection(selection);
|
|
if (useDaemonApi) {
|
|
return handleSetSelectionDaemon(
|
|
invokeServiceMethodDaemonInspectorRef('setSelectionById', selection),
|
|
uiAlreadyUpdated);
|
|
} else {
|
|
return handleSetSelectionObservatory(
|
|
invokeServiceMethodObservatoryInspectorRef(
|
|
'setSelectionById', selection),
|
|
uiAlreadyUpdated);
|
|
}
|
|
}
|
|
|
|
Future<void> handleSetSelectionObservatory(
|
|
Future<InstanceRef> setSelectionResult, bool uiAlreadyUpdated) async {
|
|
// TODO(jacobr): we need to cancel if another inspect request comes in while we are trying this one.
|
|
if (disposed) return;
|
|
final instanceRef = await setSelectionResult;
|
|
if (disposed) return;
|
|
handleSetSelectionHelper(
|
|
'true' == instanceRef?.valueAsString, uiAlreadyUpdated);
|
|
}
|
|
|
|
void handleSetSelectionHelper(bool selectionChanged, bool uiAlreadyUpdated) {
|
|
if (selectionChanged && !uiAlreadyUpdated && !disposed) {
|
|
inspectorService.notifySelectionChanged();
|
|
}
|
|
}
|
|
|
|
Future<void> handleSetSelectionDaemon(
|
|
Future<Object> setSelectionResult, bool uiAlreadyUpdated) async {
|
|
if (disposed) return;
|
|
// TODO(jacobr): we need to cancel if another inspect request comes in while we are trying this one.
|
|
final json = await setSelectionResult;
|
|
if (disposed) return;
|
|
handleSetSelectionHelper(json, uiAlreadyUpdated);
|
|
}
|
|
|
|
Future<Map<String, InstanceRef>> getEnumPropertyValues(
|
|
InspectorInstanceRef ref) async {
|
|
if (disposed) return null;
|
|
if (ref?.id == null) return null;
|
|
|
|
final instance = await getInstance(await toObservatoryInstanceRef(ref));
|
|
if (disposed || instance == null) return null;
|
|
|
|
final clazz = await inspectorLibrary.getClass(instance.classRef, this);
|
|
if (disposed || clazz == null) return null;
|
|
|
|
final Map<String, InstanceRef> properties = {};
|
|
for (FieldRef field in clazz.fields) {
|
|
final String name = field.name;
|
|
if (name.startsWith('_')) {
|
|
// Needed to filter out _deleted_enum_sentinel synthetic property.
|
|
// If showing enum values is useful we could special case
|
|
// just the _deleted_enum_sentinel property name.
|
|
continue;
|
|
}
|
|
if (name == 'values') {
|
|
// Need to filter out the synthetic 'values' member.
|
|
// TODO(jacobr): detect that this properties return type is
|
|
// different and filter that way.
|
|
continue;
|
|
}
|
|
if (field.isConst && field.isStatic) {
|
|
properties[field.name] = field.declaredType;
|
|
}
|
|
}
|
|
return properties;
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getDetailsSubtree(
|
|
RemoteDiagnosticsNode node, {
|
|
int subtreeDepth = 2,
|
|
}) async {
|
|
if (node == null) return null;
|
|
final args = {
|
|
'objectGroup': groupName,
|
|
'arg': node.dartDiagnosticRef.id,
|
|
'subtreeDepth': subtreeDepth.toString(),
|
|
};
|
|
final json = await invokeServiceMethodDaemonParams(
|
|
'getDetailsSubtree',
|
|
args,
|
|
);
|
|
return parseDiagnosticsNodeHelper(json);
|
|
}
|
|
|
|
Future<void> invokeSetFlexProperties(
|
|
InspectorInstanceRef ref,
|
|
MainAxisAlignment mainAxisAlignment,
|
|
CrossAxisAlignment crossAxisAlignment,
|
|
) async {
|
|
if (ref == null) return null;
|
|
await invokeServiceExtensionMethod(
|
|
RegistrableServiceExtension.setFlexProperties,
|
|
{
|
|
'id': ref.id,
|
|
'mainAxisAlignment': '$mainAxisAlignment',
|
|
'crossAxisAlignment': '$crossAxisAlignment',
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> invokeSetFlexFactor(
|
|
InspectorInstanceRef ref,
|
|
int flexFactor,
|
|
) async {
|
|
if (ref == null) return null;
|
|
await invokeServiceExtensionMethod(
|
|
RegistrableServiceExtension.setFlexFactor,
|
|
{'id': ref.id, 'flexFactor': '$flexFactor'},
|
|
);
|
|
}
|
|
|
|
Future<void> invokeSetFlexFit(
|
|
InspectorInstanceRef ref,
|
|
FlexFit flexFit,
|
|
) async {
|
|
if (ref == null) return null;
|
|
await invokeServiceExtensionMethod(
|
|
RegistrableServiceExtension.setFlexFit,
|
|
{'id': ref.id, 'flexFit': '$flexFit'},
|
|
);
|
|
}
|
|
|
|
Future<RemoteDiagnosticsNode> getLayoutExplorerNode(
|
|
RemoteDiagnosticsNode node, {
|
|
int subtreeDepth = 1,
|
|
}) async {
|
|
if (node == null) return null;
|
|
return parseDiagnosticsNodeDaemon(invokeServiceExtensionMethod(
|
|
RegistrableServiceExtension.getLayoutExplorerNode,
|
|
{
|
|
'groupName': groupName,
|
|
'id': node.dartDiagnosticRef.id,
|
|
'subtreeDepth': '$subtreeDepth',
|
|
},
|
|
));
|
|
}
|
|
|
|
Future<List<String>> getPubRootDirectories() async {
|
|
final List<Object> directories = await invokeServiceExtensionMethod(
|
|
RegistrableServiceExtension.getPubRootDirectories,
|
|
{},
|
|
);
|
|
return List.from(directories ?? []);
|
|
}
|
|
}
|
|
|
|
public enum FlutterTreeType {
|
|
widget, // ('Widget'),
|
|
renderObject // ('Render');
|
|
// TODO(jacobr): add semantics, and layer trees.
|
|
}
|
|
|
|
public abstract class InspectorServiceClient {
|
|
void onInspectorSelectionChanged();
|
|
|
|
void onFlutterFrame();
|
|
|
|
Future<Object> onForceRefresh();
|
|
}
|
|
|
|
/// Reference to a Dart object.
|
|
///
|
|
/// This class is similar to the Observatory protocol InstanceRef with the
|
|
/// difference that InspectorInstanceRef objects do not expire and all
|
|
/// instances of the same Dart object are guaranteed to have the same
|
|
/// InspectorInstanceRef id. The tradeoff is the consumer of
|
|
/// InspectorInstanceRef objects is responsible for managing their lifecycles.
|
|
public class InspectorInstanceRef {
|
|
public InspectorInstanceRef(string id)
|
|
{
|
|
this.id = id;
|
|
}
|
|
|
|
|
|
public override bool operator ==(object other) {
|
|
if (other is InspectorInstanceRef) {
|
|
return id == ((InspectorInstanceRef)other).id;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
public override int hashCode => id.hashCode;
|
|
|
|
|
|
public override string ToString() => id;
|
|
|
|
public readonly string id;
|
|
}
|
|
|
|
/// Manager that simplifies preventing memory leaks when using the
|
|
/// InspectorService.
|
|
///
|
|
/// This class is designed for the use case where you want to manage
|
|
/// object references associated with the current displayed UI and object
|
|
/// references associated with the candidate next frame of UI to display. Once
|
|
/// the next frame is ready, you determine whether you want to display it and
|
|
/// discard the current frame and promote the next frame to the the current
|
|
/// frame if you want to display the next frame otherwise you discard the next
|
|
/// frame.
|
|
///
|
|
/// To use this class load all data you want for the next frame by using
|
|
/// the object group specified by [next] and then if you decide to switch
|
|
/// to display that frame, call promoteNext() otherwise call clearNext().
|
|
public class InspectorObjectGroupManager {
|
|
public InspectorObjectGroupManager(InspectorService inspectorService, string debugName)
|
|
{
|
|
this.inspectorService = inspectorService;
|
|
this.debugName = debugName;
|
|
}
|
|
|
|
public readonly InspectorService inspectorService;
|
|
public readonly string debugName;
|
|
ObjectGroup _current;
|
|
ObjectGroup _next;
|
|
|
|
Completer _pendingNext;
|
|
|
|
Future pendingUpdateDone {
|
|
get
|
|
{
|
|
if (_pendingNext != null) {
|
|
return _pendingNext.future;
|
|
}
|
|
if (_next == null) {
|
|
// There is no pending update.
|
|
return Future.value();
|
|
}
|
|
_pendingNext = new Completer();
|
|
return _pendingNext.future;
|
|
}
|
|
|
|
}
|
|
|
|
ObjectGroup current {
|
|
get
|
|
{
|
|
_current = _current ??inspectorService.createObjectGroup(debugName);
|
|
return _current;
|
|
}
|
|
|
|
}
|
|
|
|
ObjectGroup next {
|
|
get
|
|
{
|
|
_next = _next ??inspectorService.createObjectGroup(debugName);
|
|
return _next;
|
|
}
|
|
|
|
}
|
|
|
|
public void clear(bool isolateStopped) {
|
|
if (isolateStopped) {
|
|
// The Dart VM will handle GCing the underlying memory.
|
|
_current = null;
|
|
_setNextNull();
|
|
} else {
|
|
clearCurrent();
|
|
cancelNext();
|
|
}
|
|
}
|
|
|
|
void promoteNext() {
|
|
clearCurrent();
|
|
_current = _next;
|
|
_setNextNull();
|
|
}
|
|
|
|
void clearCurrent() {
|
|
if (_current != null) {
|
|
_current.dispose();
|
|
_current = null;
|
|
}
|
|
}
|
|
|
|
void cancelNext() {
|
|
if (_next != null) {
|
|
_next.dispose();
|
|
_setNextNull();
|
|
}
|
|
}
|
|
|
|
void _setNextNull() {
|
|
_next = null;
|
|
if (_pendingNext != null) {
|
|
_pendingNext.complete(null);
|
|
_pendingNext = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// public class FlutterInspectorLibraryNotFound : LibraryNotFound {
|
|
// public FlutterInspectorLibraryNotFound() : base(inspectorLibraryUriCandidates)
|
|
// {
|
|
//
|
|
// }
|
|
// }
|
|
|
|
}*/
|