/*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 showOnlyUserDefined = new ValueNotifier(false); public readonly ValueNotifier expandSelectedBuildMethod = new ValueNotifier(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 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 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 selectedNode { get { return _selectedNode; } } public readonly ValueNotifier _selectedNode = new ValueNotifier((InspectorTreeNode)null); InspectorTreeNode lastExpanded; bool isActive = false; public readonly Dictionary valueToInspectorTreeNode = new Dictionary(); /// 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 _selectedErrorIndex = new ValueNotifier(null); ValueListenable 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 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 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 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 get rootDirectories => _rootDirectories ?? parent.rootDirectories; List _rootDirectories; Future 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 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 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 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 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 ); } } }*/