您最多选择25个主题
主题必须以中文或者字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
974 行
28 KiB
974 行
28 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using Unity.UIWidgets.async2;
|
|
using Unity.UIWidgets.DevTools.inspector;
|
|
using Unity.UIWidgets.foundation;
|
|
using Unity.UIWidgets.material;
|
|
using Unity.UIWidgets.ui;
|
|
using TextStyle = Unity.UIWidgets.painting.TextStyle;
|
|
|
|
namespace Unity.UIWidgets.DevTools.inspector
|
|
{
|
|
public class InspectorControllerUtils
|
|
{
|
|
public const string inspectorRefQueryParam = "inspectorRef";
|
|
|
|
public static TextStyle textStyleForLevel(DiagnosticLevel level, ColorScheme colorScheme) {
|
|
switch (level) {
|
|
case DiagnosticLevel.hidden:
|
|
return inspector_text_styles.unimportant(colorScheme);
|
|
case DiagnosticLevel.warning:
|
|
return inspector_text_styles.warning(colorScheme);
|
|
case DiagnosticLevel.error:
|
|
return inspector_text_styles.error(colorScheme);
|
|
case DiagnosticLevel.debug:
|
|
case DiagnosticLevel.info:
|
|
case DiagnosticLevel.fine:
|
|
default:
|
|
return inspector_text_styles.regular;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class InspectorSettingsController {
|
|
|
|
public readonly ValueNotifier<bool> showOnlyUserDefined = new ValueNotifier<bool>(false);
|
|
|
|
public readonly ValueNotifier<bool> expandSelectedBuildMethod = new ValueNotifier<bool>(true);
|
|
}
|
|
|
|
/// This class is based on the InspectorPanel class from the Flutter IntelliJ
|
|
/// plugin with some refactors to make it more of a true controller than a view.
|
|
public class InspectorController : DisposableController, AutoDisposeControllerMixin, InspectorServiceClient {
|
|
public InspectorController(
|
|
InspectorService inspectorService,
|
|
InspectorTreeController inspectorTree,
|
|
InspectorTreeController detailsTree,
|
|
FlutterTreeType treeType,
|
|
InspectorController parent,
|
|
bool isSummaryTree = true,
|
|
VoidCallback onExpandCollapseSupported = null,
|
|
VoidCallback onLayoutExplorerSupported = null
|
|
)
|
|
{
|
|
|
|
_treeGroups = new InspectorObjectGroupManager(inspectorService, "tree");
|
|
_selectionGroups =
|
|
new InspectorObjectGroupManager(inspectorService, "selection");
|
|
_refreshRateLimiter = RateLimiter(refreshFramesPerSecond, refresh);
|
|
|
|
assert(inspectorTree != null);
|
|
inspectorTree.config = InspectorTreeConfig(
|
|
summaryTree: isSummaryTree,
|
|
treeType: treeType,
|
|
onNodeAdded: _onNodeAdded,
|
|
onHover: highlightShowNode,
|
|
onSelectionChange: selectionChanged,
|
|
onExpand: _onExpand,
|
|
onClientActiveChange: _onClientChange,
|
|
);
|
|
if (isSummaryTree) {
|
|
details = inspector.InspectorController(
|
|
inspectorService: inspectorService,
|
|
inspectorTree: detailsTree,
|
|
treeType: treeType,
|
|
parent: this,
|
|
isSummaryTree: false,
|
|
);
|
|
} else {
|
|
details = null;
|
|
}
|
|
|
|
flutterIsolateSubscription = serviceManager.isolateManager
|
|
.getSelectedIsolate((IsolateRef flutterIsolate) {
|
|
// TODO(jacobr): listen for real isolate stopped events.
|
|
// Only send an isolate stopped event if there was a previous isolate or
|
|
// the isolate has actually changed.
|
|
if (_activeIsolate != null && _activeIsolate != flutterIsolate) {
|
|
onIsolateStopped();
|
|
}
|
|
_activeIsolate = flutterIsolate;
|
|
});
|
|
|
|
_checkForExpandCollapseSupport();
|
|
_checkForLayoutExplorerSupport();
|
|
|
|
// This logic only needs to be run once so run it in the outermost
|
|
// controller.
|
|
if (parent == null) {
|
|
// If select mode is available, enable the on device inspector as it
|
|
// won't interfere with users.
|
|
addAutoDisposeListener(_supportsToggleSelectWidgetMode, () => {
|
|
if (_supportsToggleSelectWidgetMode.value) {
|
|
serviceManager.serviceExtensionManager.setServiceExtensionState(
|
|
extensions.enableOnDeviceInspector.extension,
|
|
true,
|
|
true
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
ValueListenable<bool> get _supportsToggleSelectWidgetMode =>
|
|
serviceManager.serviceExtensionManager
|
|
.hasServiceExtension(extensions.toggleSelectWidgetMode.extension);
|
|
|
|
void _onClientChange(bool added) {
|
|
_clientCount += added ? 1 : -1;
|
|
assert(_clientCount >= 0);
|
|
if (_clientCount == 1) {
|
|
setVisibleToUser(true);
|
|
setActivate(true);
|
|
} else if (_clientCount == 0) {
|
|
setVisibleToUser(false);
|
|
}
|
|
}
|
|
|
|
int _clientCount = 0;
|
|
|
|
/// Maximum frame rate to refresh the inspector at to avoid taxing the
|
|
/// physical device with too many requests to recompute properties and trees.
|
|
///
|
|
/// A value up to around 30 frames per second could be reasonable for
|
|
/// debugging highly interactive cases particularly when the user is on a
|
|
/// simulator or high powered native device. The frame rate is set low
|
|
/// for now mainly to minimize risk.
|
|
public static readonly float refreshFramesPerSecond = 5.0f;
|
|
|
|
public readonly bool isSummaryTree;
|
|
|
|
public readonly VoidCallback onExpandCollapseSupported;
|
|
|
|
public readonly VoidCallback onLayoutExplorerSupported;
|
|
|
|
/// Parent InspectorController if this is a details subtree.
|
|
InspectorController parent;
|
|
|
|
InspectorController details;
|
|
|
|
InspectorTreeController inspectorTree;
|
|
public readonly FlutterTreeType treeType;
|
|
|
|
public readonly InspectorService inspectorService;
|
|
|
|
StreamSubscription<IsolateRef> flutterIsolateSubscription;
|
|
IsolateRef _activeIsolate;
|
|
|
|
bool _disposed = false;
|
|
|
|
RateLimiter _refreshRateLimiter;
|
|
|
|
/// Groups used to manage and cancel requests to load data to display directly
|
|
/// in the tree.
|
|
InspectorObjectGroupManager _treeGroups;
|
|
|
|
/// Groups used to manage and cancel requests to determine what the current
|
|
/// selection is.
|
|
///
|
|
/// This group needs to be kept separate from treeGroups as the selection is
|
|
/// shared more with the details subtree.
|
|
/// TODO(jacobr): is there a way we can unify the selection and tree groups?
|
|
InspectorObjectGroupManager _selectionGroups;
|
|
|
|
/// Node being highlighted due to the current hover.
|
|
InspectorTreeNode currentShowNode
|
|
{
|
|
get
|
|
{
|
|
return inspectorTree.hover;
|
|
}
|
|
set
|
|
{
|
|
inspectorTree.hover = value;
|
|
}
|
|
}
|
|
|
|
|
|
bool flutterAppFrameReady = false;
|
|
|
|
bool treeLoadStarted = false;
|
|
|
|
RemoteDiagnosticsNode subtreeRoot;
|
|
|
|
bool programaticSelectionChangeInProgress = false;
|
|
|
|
public ValueListenable<InspectorTreeNode> selectedNode
|
|
{
|
|
get
|
|
{
|
|
return _selectedNode;
|
|
}
|
|
}
|
|
|
|
public readonly ValueNotifier<InspectorTreeNode> _selectedNode = new ValueNotifier((InspectorTreeNode)null);
|
|
|
|
InspectorTreeNode lastExpanded;
|
|
|
|
bool isActive = false;
|
|
|
|
public readonly Dictionary<InspectorInstanceRef, InspectorTreeNode> valueToInspectorTreeNode = new Dictionary<InspectorInstanceRef, InspectorTreeNode>();
|
|
|
|
/// When visibleToUser is false we should dispose all allocated objects and
|
|
/// not perform any actions.
|
|
bool visibleToUser = false;
|
|
|
|
bool highlightNodesShownInBothTrees = false;
|
|
|
|
bool detailsSubtree
|
|
{
|
|
get
|
|
{
|
|
return parent != null;
|
|
}
|
|
}
|
|
|
|
RemoteDiagnosticsNode selectedDiagnostic
|
|
{
|
|
get
|
|
{
|
|
return selectedNode.value?.diagnostic;
|
|
}
|
|
}
|
|
|
|
public readonly ValueNotifier<int> _selectedErrorIndex = new ValueNotifier<int>(null);
|
|
|
|
ValueListenable<int> selectedErrorIndex
|
|
{
|
|
get
|
|
{
|
|
return _selectedErrorIndex;
|
|
}
|
|
}
|
|
|
|
FlutterTreeType getTreeType() {
|
|
return treeType;
|
|
}
|
|
|
|
void setVisibleToUser(bool visible) {
|
|
if (visibleToUser == visible) {
|
|
return;
|
|
}
|
|
visibleToUser = visible;
|
|
details?.setVisibleToUser(visible);
|
|
|
|
if (visibleToUser) {
|
|
if (parent == null) {
|
|
maybeLoadUI();
|
|
}
|
|
} else {
|
|
shutdownTree(false);
|
|
}
|
|
}
|
|
|
|
bool hasDiagnosticsValue(InspectorInstanceRef _ref) {
|
|
return valueToInspectorTreeNode.ContainsKey(_ref);
|
|
}
|
|
|
|
RemoteDiagnosticsNode findDiagnosticsValue(InspectorInstanceRef _ref) {
|
|
return valueToInspectorTreeNode[_ref]?.diagnostic;
|
|
}
|
|
|
|
void endShowNode() {
|
|
highlightShowNode(null);
|
|
}
|
|
|
|
bool highlightShowFromNodeInstanceRef(InspectorInstanceRef _ref) {
|
|
return highlightShowNode(valueToInspectorTreeNode[_ref]);
|
|
}
|
|
|
|
bool highlightShowNode(InspectorTreeNode node) {
|
|
if (node == null && parent != null) {
|
|
// If nothing is highlighted, highlight the node selected in the parent
|
|
// tree so user has context of where the node selected in the parent is
|
|
// in the details tree.
|
|
node = findMatchingInspectorTreeNode(parent.selectedDiagnostic);
|
|
}
|
|
|
|
currentShowNode = node;
|
|
return true;
|
|
}
|
|
|
|
InspectorTreeNode findMatchingInspectorTreeNode(RemoteDiagnosticsNode node) {
|
|
if (node?.valueRef == null) {
|
|
return null;
|
|
}
|
|
return valueToInspectorTreeNode[node.valueRef];
|
|
}
|
|
|
|
Future<void> getPendingUpdateDone() async {
|
|
// Wait for the selection to be resolved followed by waiting for the tree to be computed.
|
|
await _selectionGroups?.pendingUpdateDone;
|
|
await _treeGroups?.pendingUpdateDone;
|
|
// TODO(jacobr): are there race conditions we need to think mroe carefully about here?
|
|
}
|
|
|
|
Future<void> refresh() {
|
|
if (!visibleToUser) {
|
|
// We will refresh again once we are visible.
|
|
// There is a risk a refresh got triggered before the view was visble.
|
|
return Future.value();
|
|
}
|
|
|
|
// TODO(jacobr): refresh the tree as well as just the properties.
|
|
if (details != null) {
|
|
return Future.wait(
|
|
[getPendingUpdateDone(), details.getPendingUpdateDone()]);
|
|
} else {
|
|
return getPendingUpdateDone();
|
|
}
|
|
}
|
|
|
|
// Note that this may be called after the controller is disposed. We need to handle nulls in the fields.
|
|
void shutdownTree(bool isolateStopped) {
|
|
// It is critical we clear all data that is kept alive by inspector object
|
|
// references in this method as that stale data will trigger inspector
|
|
// exceptions.
|
|
programaticSelectionChangeInProgress = true;
|
|
_treeGroups?.clear(isolateStopped);
|
|
_selectionGroups?.clear(isolateStopped);
|
|
|
|
currentShowNode = null;
|
|
_selectedNode.value = null;
|
|
lastExpanded = null;
|
|
|
|
subtreeRoot = null;
|
|
|
|
inspectorTree?.root = inspectorTree?.createNode();
|
|
details?.shutdownTree(isolateStopped);
|
|
programaticSelectionChangeInProgress = false;
|
|
valueToInspectorTreeNode?.clear();
|
|
}
|
|
|
|
void onIsolateStopped() {
|
|
flutterAppFrameReady = false;
|
|
treeLoadStarted = false;
|
|
shutdownTree(true);
|
|
}
|
|
|
|
@override
|
|
Future<void> onForceRefresh() async {
|
|
assert(!_disposed);
|
|
if (!visibleToUser || _disposed) {
|
|
return Future.value();
|
|
}
|
|
await recomputeTreeRoot(null, null, false);
|
|
|
|
filterErrors();
|
|
|
|
return getPendingUpdateDone();
|
|
}
|
|
|
|
void filterErrors() {
|
|
if (isSummaryTree) {
|
|
serviceManager.errorBadgeManager.filterErrors(InspectorScreen.id,
|
|
(id) => hasDiagnosticsValue(new InspectorInstanceRef(id)));
|
|
}
|
|
}
|
|
|
|
void setActivate(bool enabled) {
|
|
if (!enabled) {
|
|
onIsolateStopped();
|
|
isActive = false;
|
|
return;
|
|
}
|
|
if (isActive) {
|
|
// Already activated.
|
|
return;
|
|
}
|
|
|
|
isActive = true;
|
|
inspectorService.addClient(this);
|
|
maybeLoadUI();
|
|
}
|
|
|
|
List<String> get rootDirectories =>
|
|
_rootDirectories ?? parent.rootDirectories;
|
|
List<String> _rootDirectories;
|
|
|
|
Future<void> maybeLoadUI() async {
|
|
if (parent != null) {
|
|
// The parent controller will drive loading the UI.
|
|
return;
|
|
}
|
|
if (!visibleToUser || !isActive) {
|
|
return;
|
|
}
|
|
|
|
if (flutterAppFrameReady) {
|
|
_rootDirectories = await inspectorService.inferPubRootDirectoryIfNeeded();
|
|
// We need to start by querying the inspector service to find out the
|
|
// current state of the UI.
|
|
|
|
final queryParams = loadQueryParams();
|
|
final inspectorRef = queryParams.containsKey(inspectorRefQueryParam)
|
|
? queryParams[inspectorRefQueryParam]
|
|
: null;
|
|
await updateSelectionFromService(
|
|
firstFrame: true, inspectorRef: inspectorRef);
|
|
} else {
|
|
final ready = await inspectorService.isWidgetTreeReady();
|
|
flutterAppFrameReady = ready;
|
|
if (isActive && ready) {
|
|
await maybeLoadUI();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> recomputeTreeRoot(
|
|
RemoteDiagnosticsNode newSelection,
|
|
RemoteDiagnosticsNode detailsSelection,
|
|
bool setSubtreeRoot, {
|
|
int subtreeDepth = 2,
|
|
}) async {
|
|
assert(!_disposed);
|
|
if (_disposed) {
|
|
return;
|
|
}
|
|
_treeGroups.cancelNext();
|
|
try {
|
|
final group = _treeGroups.next;
|
|
final node = await (detailsSubtree
|
|
? group.getDetailsSubtree(subtreeRoot, subtreeDepth: subtreeDepth)
|
|
: group.getRoot(treeType));
|
|
if (node == null || group.disposed) {
|
|
return;
|
|
}
|
|
// TODO(jacobr): as a performance optimization we should check if the
|
|
// new tree is identical to the existing tree in which case we should
|
|
// dispose the new tree and keep the old tree.
|
|
_treeGroups.promoteNext();
|
|
clearValueToInspectorTreeNodeMapping();
|
|
if (node != null) {
|
|
final InspectorTreeNode rootNode = inspectorTree.setupInspectorTreeNode(
|
|
inspectorTree.createNode(),
|
|
node,
|
|
expandChildren: true,
|
|
expandProperties: false,
|
|
);
|
|
inspectorTree.root = rootNode;
|
|
} else {
|
|
inspectorTree.root = inspectorTree.createNode();
|
|
}
|
|
refreshSelection(newSelection, detailsSelection, setSubtreeRoot);
|
|
} catch (error) {
|
|
log(error.toString(), LogLevel.error);
|
|
_treeGroups.cancelNext();
|
|
return;
|
|
}
|
|
}
|
|
|
|
void clearValueToInspectorTreeNodeMapping() {
|
|
valueToInspectorTreeNode.clear();
|
|
}
|
|
|
|
/// Show the details subtree starting with node subtreeRoot highlighting
|
|
/// node subtreeSelection.
|
|
void showDetailSubtrees(
|
|
RemoteDiagnosticsNode subtreeRoot,
|
|
RemoteDiagnosticsNode subtreeSelection
|
|
) {
|
|
this.subtreeRoot = subtreeRoot;
|
|
details?.setSubtreeRoot(subtreeRoot, subtreeSelection);
|
|
}
|
|
|
|
InspectorInstanceRef getSubtreeRootValue() {
|
|
return subtreeRoot?.valueRef;
|
|
}
|
|
|
|
void setSubtreeRoot(
|
|
RemoteDiagnosticsNode node,
|
|
RemoteDiagnosticsNode selection,
|
|
) {
|
|
assert(detailsSubtree);
|
|
selection ??= node;
|
|
if (node != null && node == subtreeRoot) {
|
|
// Select the new node in the existing subtree.
|
|
applyNewSelection(selection, null, false);
|
|
return;
|
|
}
|
|
subtreeRoot = node;
|
|
if (node == null) {
|
|
// Passing in a null node indicates we should clear the subtree and free any memory allocated.
|
|
shutdownTree(false);
|
|
return;
|
|
}
|
|
|
|
// Clear now to eliminate frame of highlighted nodes flicker.
|
|
clearValueToInspectorTreeNodeMapping();
|
|
recomputeTreeRoot(selection, null, false);
|
|
}
|
|
|
|
InspectorTreeNode getSubtreeRootNode() {
|
|
if (subtreeRoot == null) {
|
|
return null;
|
|
}
|
|
return valueToInspectorTreeNode[subtreeRoot.valueRef];
|
|
}
|
|
|
|
void refreshSelection(RemoteDiagnosticsNode newSelection,
|
|
RemoteDiagnosticsNode detailsSelection, bool setSubtreeRoot) {
|
|
newSelection ??= selectedDiagnostic;
|
|
setSelectedNode(findMatchingInspectorTreeNode(newSelection));
|
|
syncSelectionHelper(
|
|
maybeRerootDetailsTree: setSubtreeRoot,
|
|
selection: newSelection,
|
|
detailsSelection: detailsSelection,
|
|
);
|
|
|
|
if (details != null) {
|
|
if (subtreeRoot != null && getSubtreeRootNode() == null) {
|
|
subtreeRoot = newSelection;
|
|
details.setSubtreeRoot(newSelection, detailsSelection);
|
|
}
|
|
}
|
|
syncTreeSelection();
|
|
}
|
|
|
|
void syncTreeSelection() {
|
|
programaticSelectionChangeInProgress = true;
|
|
inspectorTree.selection = selectedNode.value;
|
|
inspectorTree.expandPath(selectedNode.value);
|
|
programaticSelectionChangeInProgress = false;
|
|
animateTo(selectedNode.value);
|
|
}
|
|
|
|
void selectAndShowNode(RemoteDiagnosticsNode node) {
|
|
if (node == null) {
|
|
return;
|
|
}
|
|
selectAndShowInspectorInstanceRef(node.valueRef);
|
|
}
|
|
|
|
void selectAndShowInspectorInstanceRef(InspectorInstanceRef ref) {
|
|
final node = valueToInspectorTreeNode[ref];
|
|
if (node == null) {
|
|
return;
|
|
}
|
|
setSelectedNode(node);
|
|
syncTreeSelection();
|
|
}
|
|
|
|
InspectorTreeNode getTreeNode(RemoteDiagnosticsNode node) {
|
|
if (node == null) {
|
|
return null;
|
|
}
|
|
return valueToInspectorTreeNode[node.valueRef];
|
|
}
|
|
|
|
void maybeUpdateValueUI(InspectorInstanceRef valueRef) {
|
|
var node = valueToInspectorTreeNode[valueRef];
|
|
if (node == null) {
|
|
// The value isn't shown in the parent tree. Nothing to do.
|
|
return;
|
|
}
|
|
inspectorTree.nodeChanged(node);
|
|
}
|
|
|
|
public override void onFlutterFrame() {
|
|
flutterAppFrameReady = true;
|
|
if (!visibleToUser) {
|
|
return;
|
|
}
|
|
|
|
if (!treeLoadStarted) {
|
|
treeLoadStarted = true;
|
|
// This was the first frame.
|
|
maybeLoadUI();
|
|
}
|
|
_refreshRateLimiter.scheduleRequest();
|
|
}
|
|
|
|
bool identicalDiagnosticsNodes(
|
|
RemoteDiagnosticsNode a,
|
|
RemoteDiagnosticsNode b
|
|
) {
|
|
if (a == b) {
|
|
return true;
|
|
}
|
|
if (a == null || b == null) {
|
|
return false;
|
|
}
|
|
return a.dartDiagnosticRef == b.dartDiagnosticRef;
|
|
}
|
|
|
|
@override
|
|
void onInspectorSelectionChanged() {
|
|
if (!visibleToUser) {
|
|
// Don't do anything. We will update the view once it is visible again.
|
|
return;
|
|
}
|
|
if (detailsSubtree) {
|
|
// Wait for the master to update.
|
|
return;
|
|
}
|
|
updateSelectionFromService(firstFrame: false);
|
|
}
|
|
|
|
Future<void> updateSelectionFromService(
|
|
{@required bool firstFrame, String inspectorRef}) async {
|
|
if (parent != null) {
|
|
// If we have a parent controller we should wait for the parent to update
|
|
// our selection rather than updating it our self.
|
|
return;
|
|
}
|
|
if (_selectionGroups == null) {
|
|
// Already disposed. Ignore this requested to update selection.
|
|
return;
|
|
}
|
|
treeLoadStarted = true;
|
|
_selectionGroups.cancelNext();
|
|
|
|
final group = _selectionGroups.next;
|
|
|
|
if (inspectorRef != null) {
|
|
await group.setSelectionInspector(
|
|
InspectorInstanceRef(inspectorRef),
|
|
false,
|
|
);
|
|
}
|
|
final pendingSelectionFuture = group.getSelection(
|
|
selectedDiagnostic,
|
|
treeType,
|
|
isSummaryTree: isSummaryTree,
|
|
);
|
|
|
|
final Future<RemoteDiagnosticsNode> pendingDetailsFuture = isSummaryTree
|
|
? group.getSelection(selectedDiagnostic, treeType, isSummaryTree: false)
|
|
: null;
|
|
|
|
try {
|
|
final RemoteDiagnosticsNode newSelection = await pendingSelectionFuture;
|
|
if (group.disposed) return;
|
|
RemoteDiagnosticsNode detailsSelection;
|
|
|
|
if (pendingDetailsFuture != null) {
|
|
detailsSelection = await pendingDetailsFuture;
|
|
if (group.disposed) return;
|
|
}
|
|
|
|
if (!firstFrame &&
|
|
detailsSelection?.valueRef == details?.selectedDiagnostic?.valueRef &&
|
|
newSelection?.valueRef == selectedDiagnostic?.valueRef) {
|
|
// No need to change the selection as it didn't actually change.
|
|
_selectionGroups.cancelNext();
|
|
return;
|
|
}
|
|
_selectionGroups.promoteNext();
|
|
|
|
subtreeRoot = newSelection;
|
|
|
|
applyNewSelection(newSelection, detailsSelection, true);
|
|
} catch (error) {
|
|
if (_selectionGroups.next == group) {
|
|
log(error.toString(), LogLevel.error);
|
|
_selectionGroups.cancelNext();
|
|
}
|
|
}
|
|
}
|
|
|
|
void applyNewSelection(
|
|
RemoteDiagnosticsNode newSelection,
|
|
RemoteDiagnosticsNode detailsSelection,
|
|
bool setSubtreeRoot,
|
|
) {
|
|
final InspectorTreeNode nodeInTree =
|
|
findMatchingInspectorTreeNode(newSelection);
|
|
|
|
if (nodeInTree == null) {
|
|
// The tree has probably changed since we last updated. Do a full refresh
|
|
// so that the tree includes the new node we care about.
|
|
recomputeTreeRoot(newSelection, detailsSelection, setSubtreeRoot);
|
|
}
|
|
|
|
refreshSelection(newSelection, detailsSelection, setSubtreeRoot);
|
|
}
|
|
|
|
void animateTo(InspectorTreeNode node) {
|
|
if (node == null) {
|
|
return;
|
|
}
|
|
final List<InspectorTreeNode> targets = [node];
|
|
|
|
// Backtrack to the the first non-property parent so that all properties
|
|
// for the node are visible if one property is animated to. This is helpful
|
|
// as typically users want to view the properties of a node as a chunk.
|
|
while (node.parent != null && node.diagnostic?.isProperty == true) {
|
|
node = node.parent;
|
|
}
|
|
// Make sure we scroll so that immediate un-expanded children
|
|
// are also in view. There is no risk in including these children as
|
|
// the amount of space they take up is bounded. This also ensures that if
|
|
// a node is selected, its properties will also be selected as by
|
|
// convention properties are the first children of a node and properties
|
|
// typically do not have children and are never expanded by default.
|
|
for (InspectorTreeNode child in node.children) {
|
|
final RemoteDiagnosticsNode diagnosticsNode = child.diagnostic;
|
|
targets.add(child);
|
|
if (!child.isLeaf && child.isExpanded) {
|
|
// Stop if we get to expanded children as they might be too large
|
|
// to try to scroll into view.
|
|
break;
|
|
}
|
|
if (diagnosticsNode != null && !diagnosticsNode.isProperty) {
|
|
break;
|
|
}
|
|
}
|
|
inspectorTree.animateToTargets(targets);
|
|
}
|
|
|
|
void setSelectedNode(InspectorTreeNode newSelection) {
|
|
if (newSelection == selectedNode.value) {
|
|
return;
|
|
}
|
|
|
|
_selectedNode.value = newSelection;
|
|
|
|
lastExpanded = null; // New selected node takes precedence.
|
|
endShowNode();
|
|
if (details != null) {
|
|
details.endShowNode();
|
|
} else if (parent != null) {
|
|
parent.endShowNode();
|
|
}
|
|
|
|
_updateSelectedErrorFromNode(_selectedNode.value);
|
|
animateTo(selectedNode.value);
|
|
}
|
|
|
|
/// Update the index of the selected error based on a node that has been
|
|
/// selected in the tree.
|
|
void _updateSelectedErrorFromNode(InspectorTreeNode node) {
|
|
final inspectorRef = node?.diagnostic?.valueRef?.id;
|
|
|
|
final errors = serviceManager.errorBadgeManager
|
|
.erroredItemsForPage(InspectorScreen.id)
|
|
.value;
|
|
|
|
// Check whether the node that was just selected has any errors associated
|
|
// with it.
|
|
var errorIndex = inspectorRef != null
|
|
? errors.keys.toList().indexOf(inspectorRef)
|
|
: null;
|
|
if (errorIndex == -1) {
|
|
errorIndex = null;
|
|
}
|
|
|
|
_selectedErrorIndex.value = errorIndex;
|
|
|
|
if (errorIndex != null) {
|
|
// Mark the error as "seen" as this will render slightly differently
|
|
// so the user can track which errored nodes they've viewed.
|
|
serviceManager.errorBadgeManager
|
|
.markErrorAsRead(InspectorScreen.id, errors[inspectorRef]);
|
|
// Also clear the error badge since new errors may have arrived while
|
|
// the inspector was visible (normally they're cleared when visiting
|
|
// the screen) and visiting an errored node seems an appropriate
|
|
// acknowledgement of the errors.
|
|
serviceManager.errorBadgeManager.clearErrors(InspectorScreen.id);
|
|
}
|
|
}
|
|
|
|
/// Updates the index of the selected error and selects its node in the tree.
|
|
void selectErrorByIndex(int index) {
|
|
_selectedErrorIndex.value = index;
|
|
|
|
if (index == null) return;
|
|
|
|
final errors = serviceManager.errorBadgeManager
|
|
.erroredItemsForPage(InspectorScreen.id)
|
|
.value;
|
|
|
|
updateSelectionFromService(
|
|
firstFrame: false, inspectorRef: errors.keys.elementAt(index));
|
|
}
|
|
|
|
void _onExpand(InspectorTreeNode node) {
|
|
inspectorTree.maybePopulateChildren(node);
|
|
}
|
|
|
|
void selectionChanged() {
|
|
if (visibleToUser == false) {
|
|
return;
|
|
}
|
|
|
|
InspectorTreeNode node = inspectorTree.selection;
|
|
if (node != null) {
|
|
inspectorTree.maybePopulateChildren(node);
|
|
}
|
|
if (programaticSelectionChangeInProgress) {
|
|
return;
|
|
}
|
|
if (node != null) {
|
|
setSelectedNode(node);
|
|
bool maybeReroot = isSummaryTree &&
|
|
details != null &&
|
|
selectedDiagnostic != null &&
|
|
!details.hasDiagnosticsValue(selectedDiagnostic.valueRef);
|
|
syncSelectionHelper(
|
|
maybeRerootDetailsTree: maybeReroot,
|
|
selection: selectedDiagnostic,
|
|
detailsSelection: selectedDiagnostic,
|
|
);
|
|
|
|
if (!maybeReroot) {
|
|
if (isSummaryTree && details != null) {
|
|
details.selectAndShowNode(selectedDiagnostic);
|
|
} else if (parent != null) {
|
|
parent
|
|
.selectAndShowNode(firstAncestorInParentTree(selectedNode.value));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RemoteDiagnosticsNode firstAncestorInParentTree(InspectorTreeNode node) {
|
|
if (parent == null) {
|
|
return node.diagnostic;
|
|
}
|
|
while (node != null) {
|
|
var diagnostic = node.diagnostic;
|
|
if (diagnostic != null &&
|
|
parent.hasDiagnosticsValue(diagnostic.valueRef)) {
|
|
return parent.findDiagnosticsValue(diagnostic.valueRef);
|
|
}
|
|
node = node.parent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void syncSelectionHelper(
|
|
bool maybeRerootDetailsTree = true,
|
|
RemoteDiagnosticsNode selection = null,
|
|
RemoteDiagnosticsNode detailsSelection = null
|
|
) {
|
|
if (selection != null) {
|
|
if (selection.isCreatedByLocalProject) {
|
|
_navigateTo(selection);
|
|
}
|
|
}
|
|
if (detailsSubtree || details == null) {
|
|
if (selection != null) {
|
|
var toSelect = selectedNode.value;
|
|
|
|
while (toSelect != null && toSelect.diagnostic.isProperty) {
|
|
toSelect = toSelect.parent;
|
|
}
|
|
|
|
if (toSelect != null) {
|
|
var diagnosticToSelect = toSelect.diagnostic;
|
|
diagnosticToSelect.setSelectionInspector(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (maybeRerootDetailsTree) {
|
|
showDetailSubtrees(selection, detailsSelection);
|
|
} else if (selection != null) {
|
|
// We can't rely on the details tree to update the selection on the server in this case.
|
|
selection.setSelectionInspector(true);
|
|
}
|
|
}
|
|
|
|
void _navigateTo(RemoteDiagnosticsNode diagnostic) {
|
|
// TODO(jacobr): dispatch an event over the inspectorService requesting a
|
|
// navigate operation.
|
|
}
|
|
|
|
public override void dispose() {
|
|
D.assert(!_disposed);
|
|
_disposed = true;
|
|
flutterIsolateSubscription.cancel();
|
|
if (inspectorService != null) {
|
|
shutdownTree(false);
|
|
}
|
|
_treeGroups?.clear(false);
|
|
_treeGroups = null;
|
|
_selectionGroups?.clear(false);
|
|
_selectionGroups = null;
|
|
details?.dispose();
|
|
base.dispose();
|
|
}
|
|
|
|
static string treeTypeDisplayName(FlutterTreeType treeType) {
|
|
switch (treeType) {
|
|
case FlutterTreeType.widget:
|
|
return "Widget";
|
|
case FlutterTreeType.renderObject:
|
|
return "Render Objects";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void _onNodeAdded(
|
|
InspectorTreeNode node,
|
|
RemoteDiagnosticsNode diagnosticsNode
|
|
) {
|
|
InspectorInstanceRef valueRef = diagnosticsNode.valueRef;
|
|
// Properties do not have unique values so should not go in the valueToInspectorTreeNode map.
|
|
if (valueRef.id != null && !diagnosticsNode.isProperty) {
|
|
valueToInspectorTreeNode[valueRef] = node;
|
|
}
|
|
}
|
|
|
|
Future expandAllNodesInDetailsTree() {
|
|
details.recomputeTreeRoot(
|
|
inspectorTree.selection?.diagnostic,
|
|
details.inspectorTree.selection?.diagnostic ??
|
|
details.inspectorTree.root?.diagnostic,
|
|
false,
|
|
subtreeDepth: maxJsInt
|
|
);
|
|
}
|
|
|
|
Future collapseDetailsToSelected() {
|
|
details.inspectorTree.collapseToSelected();
|
|
details.animateTo(details.inspectorTree.selection);
|
|
return null;
|
|
}
|
|
|
|
/// execute given [callback] when minimum Flutter [version] is met.
|
|
void _onVersionSupported(
|
|
SemanticVersion version,
|
|
VoidCallback callback
|
|
) {
|
|
final flutterVersionServiceListenable = serviceManager
|
|
.registeredServiceListenable(registrations.flutterVersion.service);
|
|
addAutoDisposeListener(flutterVersionServiceListenable, () async {
|
|
final registered = flutterVersionServiceListenable.value;
|
|
if (registered) {
|
|
final flutterVersion =
|
|
FlutterVersion.parse((await serviceManager.flutterVersion).json);
|
|
if (flutterVersion.isSupported(supportedVersion: version)) {
|
|
callback();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _checkForExpandCollapseSupport() {
|
|
if (onExpandCollapseSupported == null) return;
|
|
// Configurable subtree depth is available in versions of Flutter
|
|
// greater than or equal to 1.9.7, but the flutterVersion service is
|
|
// not available until 1.10.1, so we will check for 1.10.1 here.
|
|
_onVersionSupported(
|
|
SemanticVersion(major: 1, minor: 10, patch: 1),
|
|
onExpandCollapseSupported
|
|
);
|
|
}
|
|
|
|
void _checkForLayoutExplorerSupport() {
|
|
if (onLayoutExplorerSupported == null) return;
|
|
_onVersionSupported(
|
|
SemanticVersion(major: 1, minor: 13, patch: 1),
|
|
onLayoutExplorerSupported
|
|
);
|
|
}
|
|
}
|
|
}
|