io.flutter.preview.PreviewView.java Source code

Java tutorial

Introduction

Here is the source code for io.flutter.preview.PreviewView.java

Source

/*
 * Copyright 2018 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
package io.flutter.preview;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.gson.JsonObject;
import com.intellij.icons.AllIcons;
import com.intellij.ide.*;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.CaretEvent;
import com.intellij.openapi.editor.event.CaretListener;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.SimpleToolWindowPanel;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ex.ToolWindowEx;
import com.intellij.ui.*;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.labels.LinkLabel;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.content.ContentManager;
import com.intellij.ui.speedSearch.SpeedSearchUtil;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.messages.MessageBusConnection;
import com.jetbrains.lang.dart.analyzer.DartAnalysisServerService;
import com.jetbrains.lang.dart.assists.AssistUtils;
import com.jetbrains.lang.dart.assists.DartSourceEditException;
import icons.FlutterIcons;
import io.flutter.FlutterInitializer;
import io.flutter.FlutterMessages;
import io.flutter.FlutterUtils;
import io.flutter.console.FlutterConsoles;
import io.flutter.dart.FlutterDartAnalysisServer;
import io.flutter.dart.FlutterOutlineListener;
import io.flutter.inspector.FlutterWidget;
import io.flutter.settings.FlutterSettings;
import io.flutter.utils.CustomIconMaker;
import net.miginfocom.swing.MigLayout;
import org.dartlang.analysis.server.protocol.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;

@com.intellij.openapi.components.State(name = "FlutterPreviewView", storages = { @Storage("$WORKSPACE_FILE$") })
public class PreviewView implements PersistentStateComponent<PreviewViewState>, Disposable {
    public static final String TOOL_WINDOW_ID = "Flutter Outline";

    public static final String FEEDBACK_URL = "https://goo.gl/forms/MbPU0kcPqBO6tunH3";

    @NotNull
    private final PreviewViewState state = new PreviewViewState();

    @NotNull
    private final Project project;

    @NotNull
    private final FlutterDartAnalysisServer flutterAnalysisServer;

    final QuickAssistAction actionCenter;
    final QuickAssistAction actionPadding;
    final QuickAssistAction actionColumn;
    final QuickAssistAction actionRow;
    final QuickAssistAction actionMoveUp;
    final QuickAssistAction actionMoveDown;
    final QuickAssistAction actionRemove;
    final ExtractMethodAction actionExtractMethod;

    private SimpleToolWindowPanel windowPanel;
    private ActionToolbar windowToolbar;

    private final Map<String, AnAction> messageToActionMap = new HashMap<>();
    private final Map<AnAction, SourceChange> actionToChangeMap = new HashMap<>();

    private boolean isSettingSplitterProportion = false;
    private Splitter splitter;
    private JScrollPane scrollPane;
    private OutlineTree tree;
    private PreviewArea previewArea;

    private final Set<FlutterOutline> outlinesWithWidgets = Sets.newHashSet();
    private final Map<FlutterOutline, DefaultMutableTreeNode> outlineToNodeMap = Maps.newHashMap();

    private VirtualFile currentFile;
    private String currentFilePath;
    FileEditor currentFileEditor;
    private Editor currentEditor;
    private FlutterOutline currentOutline;

    private final RenderHelper myRenderHelper;

    private final FlutterOutlineListener outlineListener = new FlutterOutlineListener() {
        @Override
        public void outlineUpdated(@NotNull String filePath, @NotNull FlutterOutline outline,
                @Nullable String instrumentedCode) {
            if (Objects.equals(currentFilePath, filePath)) {
                ApplicationManager.getApplication().invokeLater(() -> updateOutline(outline));
                if (myRenderHelper != null && previewArea != null) {
                    previewArea.renderingStarted();
                    myRenderHelper.setFile(currentFile, outline, instrumentedCode);
                    ApplicationManager.getApplication().invokeLater(() -> {
                        final Caret caret = currentEditor.getCaretModel().getPrimaryCaret();
                        myRenderHelper.setOffset(caret.getOffset());
                    });
                }
            }
        }
    };

    private final CaretListener caretListener = new CaretListener() {
        @Override
        public void caretPositionChanged(CaretEvent e) {
            final Caret caret = e.getCaret();
            if (caret != null) {
                ApplicationManager.getApplication().invokeLater(() -> applyEditorSelectionToTree(caret));
            }
        }

        @Override
        public void caretAdded(CaretEvent e) {
        }

        @Override
        public void caretRemoved(CaretEvent e) {
        }
    };

    private final TreeSelectionListener treeSelectionListener = this::handleTreeSelectionEvent;

    final RenderHelper.Listener renderListener = new RenderHelper.Listener() {
        @Override
        public void onSchedule(@NotNull FlutterOutline widget) {
            // If rendering is fast, showing message is not nice.
            //ApplicationManager.getApplication().invokeLater(() -> {
            //  previewArea.clear("Rendering...");
            //});
        }

        @Override
        public void onResponse(@NotNull FlutterOutline widget, @NotNull JsonObject response) {
            ApplicationManager.getApplication().invokeLater(() -> {
                if (previewArea != null) {
                    previewArea.show(currentOutline, widget, response);

                    final Caret caret = currentEditor.getCaretModel().getPrimaryCaret();
                    final FlutterOutline outline = findOutlineAtOffset(currentOutline, caret.getOffset());
                    if (outline != null) {
                        previewArea.select(ImmutableList.of(outline));
                    }
                }
            });
        }

        @Override
        public void onFailure(@NotNull RenderProblemKind kind, @Nullable FlutterOutline widget) {
            ApplicationManager.getApplication().invokeLater(() -> {
                if (previewArea != null) {
                    switch (kind) {
                    case NO_WIDGET:
                        previewArea.clear(PreviewArea.NO_WIDGET_MESSAGE);
                        minimizePreviewArea();
                        break;
                    case NOT_RENDERABLE_WIDGET:
                        assert widget != null;
                        showNotRenderableInPreviewArea(widget);
                        break;
                    case TIMEOUT_START:
                        previewArea.clear("Timeout during start");
                        break;
                    case TIMEOUT_RENDER:
                        previewArea.clear("Timeout during rendering");
                        break;
                    case INVALID_JSON:
                        previewArea.clear("Invalid JSON response");
                        break;
                    }
                }
            });
        }

        @Override
        public void onRenderableWidget(@NotNull FlutterOutline widget) {
            setSplitterProportion(getState().getSplitterProportion());
            final Dimension renderSize = previewArea.getRenderSize();
            myRenderHelper.setSize(renderSize.width, renderSize.height);
        }

        @Override
        public void onLocalException(@NotNull FlutterOutline widget, @NotNull Throwable localException) {
            ApplicationManager.getApplication().invokeLater(() -> showLocalException(localException));
        }

        @Override
        public void onRemoteException(@NotNull FlutterOutline widget, @NotNull JsonObject remoteException) {
            ApplicationManager.getApplication().invokeLater(() -> showRemoteException(remoteException));
        }
    };

    private void minimizePreviewArea() {
        // TODO(devoncarew): We should size the preview area to a fixed, smaller size (based on the largest
        //                   non-preview sized content).
        setSplitterProportion(0.85f);
    }

    public PreviewView(@NotNull Project project) {
        this.project = project;

        flutterAnalysisServer = FlutterDartAnalysisServer.getInstance(project);

        if (FlutterSettings.getInstance().isShowPreviewArea()) {
            myRenderHelper = new RenderHelper(project, renderListener);
        } else {
            myRenderHelper = null;
        }

        // Show preview for the file selected when the view is being opened.
        final VirtualFile[] selectedFiles = FileEditorManager.getInstance(project).getSelectedFiles();
        if (selectedFiles.length != 0) {
            setSelectedFile(selectedFiles[0]);
        }

        final FileEditor[] selectedEditors = FileEditorManager.getInstance(project).getSelectedEditors();
        if (selectedEditors.length != 0) {
            setSelectedEditor(selectedEditors[0]);
        }

        // Listen for selecting files.
        final MessageBusConnection bus = project.getMessageBus().connect(project);
        bus.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() {
            @Override
            public void selectionChanged(@NotNull FileEditorManagerEvent event) {
                setSelectedFile(event.getNewFile());
                setSelectedEditor(event.getNewEditor());
            }
        });

        actionCenter = new QuickAssistAction("dart.assist.flutter.wrap.center", FlutterIcons.Center,
                "Center widget");
        actionPadding = new QuickAssistAction("dart.assist.flutter.wrap.padding", FlutterIcons.Padding,
                "Add padding");
        actionColumn = new QuickAssistAction("dart.assist.flutter.wrap.column", FlutterIcons.Column,
                "Wrap with Column");
        actionRow = new QuickAssistAction("dart.assist.flutter.wrap.row", FlutterIcons.Row, "Wrap with Row");
        actionMoveUp = new QuickAssistAction("dart.assist.flutter.move.up", FlutterIcons.Up, "Move widget up");
        actionMoveDown = new QuickAssistAction("dart.assist.flutter.move.down", FlutterIcons.Down,
                "Move widget down");
        actionRemove = new QuickAssistAction("dart.assist.flutter.removeWidget", FlutterIcons.RemoveWidget,
                "Remove widget");
        actionExtractMethod = new ExtractMethodAction();
    }

    @Override
    public void dispose() {
    }

    @NotNull
    @Override
    public PreviewViewState getState() {
        return this.state;
    }

    @Override
    public void loadState(@NotNull PreviewViewState state) {
        this.state.copyFrom(state);
    }

    public void initToolWindow(@NotNull ToolWindow toolWindow) {
        final ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
        final ContentManager contentManager = toolWindow.getContentManager();

        final DefaultActionGroup toolbarGroup = new DefaultActionGroup();
        toolbarGroup.add(actionCenter);
        toolbarGroup.add(actionPadding);
        toolbarGroup.add(actionColumn);
        toolbarGroup.add(actionRow);
        toolbarGroup.addSeparator();
        toolbarGroup.add(actionExtractMethod);
        toolbarGroup.addSeparator();
        toolbarGroup.add(actionMoveUp);
        toolbarGroup.add(actionMoveDown);
        toolbarGroup.addSeparator();
        toolbarGroup.add(actionRemove);
        toolbarGroup.add(new ShowOnlyWidgetsAction(AllIcons.General.Filter, "Show only widgets"));

        final Content content = contentFactory.createContent(null, null, false);
        content.setCloseable(false);

        windowPanel = new OutlineComponent(this);
        content.setComponent(windowPanel);

        windowToolbar = ActionManager.getInstance().createActionToolbar("PreviewViewToolbar", toolbarGroup, true);
        windowPanel.setToolbar(windowToolbar.getComponent());

        final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();

        tree = new OutlineTree(rootNode);
        tree.setCellRenderer(new OutlineTreeCellRenderer());
        tree.expandAll();

        initTreePopup();

        // Add collapse all, expand all, and feedback buttons.
        if (toolWindow instanceof ToolWindowEx) {
            final AnAction sendFeedbackAction = new AnAction("Send Feedback", "Send Feedback",
                    FlutterIcons.Feedback) {
                @Override
                public void actionPerformed(AnActionEvent event) {
                    BrowserUtil.browse(FEEDBACK_URL);
                }
            };

            final AnAction separator = new AnAction(AllIcons.General.Divider) {
                @Override
                public void actionPerformed(AnActionEvent event) {
                }
            };

            final TreeExpander expander = new DefaultTreeExpander(tree);
            final CommonActionsManager actions = CommonActionsManager.getInstance();

            final AnAction expandAllAction = actions.createExpandAllAction(expander, tree);
            expandAllAction.getTemplatePresentation().setIcon(AllIcons.General.ExpandAll);

            final AnAction collapseAllAction = actions.createCollapseAllAction(expander, tree);
            collapseAllAction.getTemplatePresentation().setIcon(AllIcons.General.CollapseAll);

            ((ToolWindowEx) toolWindow).setTitleActions(sendFeedbackAction, separator, expandAllAction,
                    collapseAllAction);
        }

        new TreeSpeedSearch(tree) {
            @Override
            protected String getElementText(Object element) {
                final TreePath path = (TreePath) element;
                final DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
                final Object object = node.getUserObject();
                if (object instanceof OutlineObject) {
                    return ((OutlineObject) object).getSpeedSearchString();
                }
                return null;
            }
        };

        tree.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() > 1) {
                    final TreePath selectionPath = tree.getSelectionPath();
                    if (selectionPath != null) {
                        selectPath(selectionPath, true);
                    }
                }
            }
        });

        tree.addTreeSelectionListener(treeSelectionListener);

        scrollPane = ScrollPaneFactory.createScrollPane(tree);

        previewArea = new PreviewArea(new PreviewArea.Listener() {
            @Override
            public void clicked(FlutterOutline outline) {
                final ImmutableList<FlutterOutline> outlines = ImmutableList.of(outline);
                updateActionsForOutlines(outlines);
                applyOutlinesSelectionToTree(outlines);
                jumpToOutlineInEditor(outline, false);
            }

            @Override
            public void doubleClicked(FlutterOutline outline) {
                final ImmutableList<FlutterOutline> outlines = ImmutableList.of(outline);
                updateActionsForOutlines(outlines);
                applyOutlinesSelectionToTree(outlines);
                jumpToOutlineInEditor(outline, true);
            }

            @Override
            public void resized(int width, int height) {
                if (myRenderHelper != null) {
                    myRenderHelper.setSize(width, height);
                }
            }
        });

        splitter = new Splitter(true);
        setSplitterProportion(getState().getSplitterProportion());
        getState().addListener(e -> {
            final float newProportion = getState().getSplitterProportion();
            if (splitter.getProportion() != newProportion) {
                setSplitterProportion(newProportion);
            }
        });
        //noinspection Convert2Lambda
        splitter.addPropertyChangeListener("proportion", new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (!isSettingSplitterProportion) {
                    getState().setSplitterProportion(splitter.getProportion());
                }
            }
        });
        splitter.setFirstComponent(scrollPane);
        windowPanel.setContent(splitter);

        contentManager.addContent(content);
        contentManager.setSelectedContent(content);
    }

    private void initTreePopup() {
        tree.addMouseListener(new PopupHandler() {
            public void invokePopup(Component comp, int x, int y) {
                // Ensure that at least one Widget item is selected.
                final List<FlutterOutline> selectedOutlines = getOutlinesSelectedInTree();
                if (selectedOutlines.isEmpty()) {
                    return;
                }
                for (FlutterOutline outline : selectedOutlines) {
                    if (outline.getDartElement() != null) {
                        return;
                    }
                }

                // The corresponding tree item has just been selected.
                // Wait short time for receiving assists from the server.
                for (int i = 0; i < 20 && actionToChangeMap.isEmpty(); i++) {
                    Uninterruptibles.sleepUninterruptibly(5, TimeUnit.MILLISECONDS);
                }

                final DefaultActionGroup group = new DefaultActionGroup();
                boolean hasAction = false;
                if (actionCenter.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionCenter));
                }
                if (actionPadding.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionPadding));
                }
                if (actionColumn.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionColumn));
                }
                if (actionRow.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionRow));
                }
                group.addSeparator();
                if (actionExtractMethod.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionExtractMethod));
                }
                group.addSeparator();
                if (actionMoveUp.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionMoveUp));
                }
                if (actionMoveDown.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionMoveDown));
                }
                group.addSeparator();
                if (actionRemove.isEnabled()) {
                    hasAction = true;
                    group.add(new TextOnlyActionWrapper(actionRemove));
                }

                // Don't show the empty popup.
                if (!hasAction) {
                    return;
                }

                final ActionManager actionManager = ActionManager.getInstance();
                final ActionPopupMenu popupMenu = actionManager.createActionPopupMenu(ActionPlaces.UNKNOWN, group);
                popupMenu.getComponent().show(comp, x, y);
            }
        });
    }

    private void handleTreeSelectionEvent(TreeSelectionEvent e) {
        final TreePath selectionPath = e.getNewLeadSelectionPath();
        if (selectionPath != null) {
            ApplicationManager.getApplication().invokeLater(() -> selectPath(selectionPath, false));
        }

        final List<FlutterOutline> selectedOutlines = getOutlinesSelectedInTree();
        updateActionsForOutlines(selectedOutlines);
    }

    private void selectPath(TreePath selectionPath, boolean focusEditor) {
        final FlutterOutline outline = getOutlineOfPath(selectionPath);
        jumpToOutlineInEditor(outline, focusEditor);
    }

    private void jumpToOutlineInEditor(FlutterOutline outline, boolean focusEditor) {
        if (outline == null) {
            return;
        }
        final int offset = outline.getDartElement() != null ? outline.getDartElement().getLocation().getOffset()
                : outline.getOffset();
        final int editorOffset = getConvertedFileOffset(offset);

        sendAnalyticEvent("jumpToSource");

        if (currentFile != null) {
            currentEditor.getCaretModel().removeCaretListener(caretListener);
            try {
                new OpenFileDescriptor(project, currentFile, editorOffset).navigate(focusEditor);
            } finally {
                currentEditor.getCaretModel().addCaretListener(caretListener);
            }
        }

        if (myRenderHelper != null) {
            myRenderHelper.setOffset(editorOffset);
            previewArea.select(ImmutableList.of(outline));
        }
    }

    private void updateActionsForOutlines(List<FlutterOutline> outlines) {
        synchronized (actionToChangeMap) {
            actionToChangeMap.clear();
        }

        final VirtualFile selectionFile = this.currentFile;
        if (selectionFile != null && !outlines.isEmpty()) {
            ApplicationManager.getApplication().executeOnPooledThread(() -> {
                final FlutterOutline firstOutline = outlines.get(0);
                final FlutterOutline lastOutline = outlines.get(outlines.size() - 1);
                final int offset = getConvertedOutlineOffset(firstOutline);
                final int length = getConvertedOutlineEnd(lastOutline) - offset;
                final List<SourceChange> changes = flutterAnalysisServer.edit_getAssists(selectionFile, offset,
                        length);

                // If the current file or outline are different, ignore the changes.
                // We will eventually get new changes.
                final List<FlutterOutline> newOutlines = getOutlinesSelectedInTree();
                if (!Objects.equals(this.currentFile, selectionFile) || !outlines.equals(newOutlines)) {
                    return;
                }

                // Associate changes with actions.
                // Actions will be enabled / disabled in background.
                for (SourceChange change : changes) {
                    final AnAction action = messageToActionMap.get(change.getMessage());
                    if (action != null) {
                        actionToChangeMap.put(action, change);
                    }
                }

                // Update actions immediately.
                if (windowToolbar != null) {
                    ApplicationManager.getApplication().invokeLater(() -> windowToolbar.updateActionsImmediately());
                }
            });
        }
    }

    // TODO: Add parent relationship info to FlutterOutline instead of this O(n^2) traversal.
    private Element getElementParentFor(@Nullable Element element) {
        if (element == null) {
            return null;
        }

        for (FlutterOutline outline : outlineToNodeMap.keySet()) {
            final List<FlutterOutline> children = outline.getChildren();
            if (children != null) {
                for (FlutterOutline child : children) {
                    if (child.getDartElement() == element) {
                        return outline.getDartElement();
                    }
                }
            }
        }

        return null;
    }

    private DefaultTreeModel getTreeModel() {
        return (DefaultTreeModel) tree.getModel();
    }

    private DefaultMutableTreeNode getRootNode() {
        return (DefaultMutableTreeNode) getTreeModel().getRoot();
    }

    private void updateOutline(@NotNull FlutterOutline outline) {
        currentOutline = outline;

        final DefaultMutableTreeNode rootNode = getRootNode();
        rootNode.removeAllChildren();

        outlinesWithWidgets.clear();
        outlineToNodeMap.clear();
        if (outline.getChildren() != null) {
            computeOutlinesWithWidgets(outline);
            updateOutlineImpl(rootNode, outline.getChildren());
        }

        getTreeModel().reload(rootNode);
        tree.expandAll();

        if (currentEditor != null) {
            final Caret caret = currentEditor.getCaretModel().getPrimaryCaret();
            applyEditorSelectionToTree(caret);
        }

        if (FlutterSettings.getInstance().isShowPreviewArea()) {
            if (ModelUtils.containsBuildMethod(outline)) {
                splitter.setSecondComponent(previewArea.getComponent());
            } else {
                splitter.setSecondComponent(null);
            }
        }
    }

    private boolean computeOutlinesWithWidgets(FlutterOutline outline) {
        boolean hasWidget = false;
        if (outline.getDartElement() == null) {
            outlinesWithWidgets.add(outline);
            hasWidget = true;
        }

        final List<FlutterOutline> children = outline.getChildren();
        if (children != null) {
            for (final FlutterOutline child : children) {
                if (computeOutlinesWithWidgets(child)) {
                    outlinesWithWidgets.add(outline);
                    hasWidget = true;
                }
            }
        }
        return hasWidget;
    }

    private void updateOutlineImpl(@NotNull DefaultMutableTreeNode parent, @NotNull List<FlutterOutline> outlines) {
        int index = 0;
        for (final FlutterOutline outline : outlines) {
            if (FlutterSettings.getInstance().isShowOnlyWidgets() && !outlinesWithWidgets.contains(outline)) {
                continue;
            }

            final OutlineObject object = new OutlineObject(outline);
            final DefaultMutableTreeNode node = new DefaultMutableTreeNode(object);
            outlineToNodeMap.put(outline, node);

            getTreeModel().insertNodeInto(node, parent, index++);

            if (outline.getChildren() != null) {
                updateOutlineImpl(node, outline.getChildren());
            }
        }
    }

    @NotNull
    private List<FlutterOutline> getOutlinesSelectedInTree() {
        final List<FlutterOutline> selectedOutlines = new ArrayList<>();
        final DefaultMutableTreeNode[] selectedNodes = tree.getSelectedNodes(DefaultMutableTreeNode.class, null);
        for (DefaultMutableTreeNode selectedNode : selectedNodes) {
            final FlutterOutline outline = getOutlineOfNode(selectedNode);
            selectedOutlines.add(outline);
        }
        return selectedOutlines;
    }

    static private FlutterOutline getOutlineOfNode(DefaultMutableTreeNode node) {
        final OutlineObject object = (OutlineObject) node.getUserObject();
        return object.outline;
    }

    @Nullable
    private FlutterOutline getOutlineOfPath(@Nullable TreePath path) {
        if (path == null) {
            return null;
        }

        final DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
        return getOutlineOfNode(node);
    }

    private int getConvertedFileOffset(int offset) {
        return DartAnalysisServerService.getInstance(project).getConvertedOffset(currentFile, offset);
    }

    private int getConvertedOutlineOffset(FlutterOutline outline) {
        final int offset = outline.getOffset();
        return getConvertedFileOffset(offset);
    }

    private int getConvertedOutlineEnd(FlutterOutline outline) {
        final int end = outline.getOffset() + outline.getLength();
        return getConvertedFileOffset(end);
    }

    private FlutterOutline findOutlineAtOffset(FlutterOutline outline, int offset) {
        if (outline == null) {
            return null;
        }
        if (getConvertedOutlineOffset(outline) <= offset && offset <= getConvertedOutlineEnd(outline)) {
            if (outline.getChildren() != null) {
                for (FlutterOutline child : outline.getChildren()) {
                    final FlutterOutline foundChild = findOutlineAtOffset(child, offset);
                    if (foundChild != null) {
                        return foundChild;
                    }
                }
            }
            return outline;
        }
        return null;
    }

    private void addOutlinesCoveredByRange(List<FlutterOutline> covered, int start, int end,
            @Nullable FlutterOutline outline) {
        if (outline == null) {
            return;
        }

        final int outlineStart = getConvertedOutlineOffset(outline);
        final int outlineEnd = getConvertedOutlineEnd(outline);
        // The outline ends before, or starts after the selection.
        if (outlineEnd < start || outlineStart > end) {
            return;
        }
        // The outline is covered by the selection.
        if (outlineStart >= start && outlineEnd <= end) {
            covered.add(outline);
            return;
        }
        // The outline covers the selection.
        if (outlineStart <= start && end <= outlineEnd) {
            if (outline.getChildren() != null) {
                for (FlutterOutline child : outline.getChildren()) {
                    addOutlinesCoveredByRange(covered, start, end, child);
                }
            }
        }
    }

    private void setSelectedFile(VirtualFile newFile) {
        if (currentFile != null) {
            flutterAnalysisServer.removeOutlineListener(currentFilePath, outlineListener);
            currentFile = null;
            currentFilePath = null;
        }

        // If not a Dart file, ignore it.
        if (newFile != null && !FlutterUtils.isDartFile(newFile)) {
            newFile = null;
        }

        // Show the toolbar if the new file is a Dart file, or hide otherwise.
        if (windowPanel != null) {
            if (newFile != null) {
                windowPanel.setToolbar(windowToolbar.getComponent());
            } else if (windowPanel.isToolbarVisible()) {
                windowPanel.setToolbar(null);
            }
        }

        // If the tree is already created, clear it now, until the outline for the new file is received.
        if (tree != null) {
            final DefaultMutableTreeNode rootNode = getRootNode();
            rootNode.removeAllChildren();
            getTreeModel().reload(rootNode);
        }

        // Set the new file, without outline.
        if (myRenderHelper != null) {
            myRenderHelper.setFile(newFile, null, null);
            if (newFile == null && previewArea != null) {
                previewArea.clear(PreviewArea.NOTHING_TO_SHOW);
            }
        }

        // Subscribe for the outline for the new file.
        if (newFile != null) {
            currentFile = newFile;
            currentFilePath = FileUtil.toSystemDependentName(currentFile.getPath());
            flutterAnalysisServer.addOutlineListener(currentFilePath, outlineListener);
        }
    }

    private void setSelectedEditor(FileEditor newEditor) {
        if (currentEditor != null) {
            currentEditor.getCaretModel().removeCaretListener(caretListener);
        }
        if (newEditor instanceof TextEditor) {
            currentFileEditor = newEditor;
            currentEditor = ((TextEditor) newEditor).getEditor();
            currentEditor.getCaretModel().addCaretListener(caretListener);
        }
    }

    private void applyEditorSelectionToTree(Caret caret) {
        final List<FlutterOutline> selectedOutlines = new ArrayList<>();

        // Try to find outlines covered by the selection.
        addOutlinesCoveredByRange(selectedOutlines, caret.getSelectionStart(), caret.getSelectionEnd(),
                currentOutline);

        // If no covered outlines, try to find the outline under the caret.
        if (selectedOutlines.isEmpty()) {
            final FlutterOutline outline = findOutlineAtOffset(currentOutline, caret.getOffset());
            if (outline != null) {
                selectedOutlines.add(outline);
            }
        }

        updateActionsForOutlines(selectedOutlines);
        applyOutlinesSelectionToTree(selectedOutlines);

        if (myRenderHelper != null) {
            final int offset = caret.getOffset();
            myRenderHelper.setOffset(offset);
            previewArea.select(selectedOutlines);
        }
    }

    private void applyOutlinesSelectionToTree(List<FlutterOutline> outlines) {
        final List<TreePath> selectedPaths = new ArrayList<>();
        TreeNode[] lastNodePath = null;
        TreePath lastTreePath = null;
        for (FlutterOutline outline : outlines) {
            final DefaultMutableTreeNode selectedNode = outlineToNodeMap.get(outline);
            if (selectedNode != null) {
                lastNodePath = selectedNode.getPath();
                lastTreePath = new TreePath(lastNodePath);
                selectedPaths.add(lastTreePath);
            }
        }

        if (lastNodePath != null) {
            // Ensure that all parent nodes are expected.
            tree.scrollPathToVisible(lastTreePath);

            // Ensure that the top-level declaration (class) is on the top of the tree.
            if (lastNodePath.length >= 2) {
                scrollTreeToNodeOnTop(lastNodePath[1]);
            }

            // Ensure that the selected node is still visible, even if the top-level declaration is long.
            tree.scrollPathToVisible(lastTreePath);
        }

        // Now actually select the node.
        tree.removeTreeSelectionListener(treeSelectionListener);
        tree.setSelectionPaths(selectedPaths.toArray(new TreePath[0]));
        tree.addTreeSelectionListener(treeSelectionListener);

        // JTree attempts to show as much of the node as possible, so scrolls horizontally.
        // But we actually need to see the whole hierarchy, so we scroll back to zero.
        scrollPane.getHorizontalScrollBar().setValue(0);
    }

    private void scrollTreeToNodeOnTop(TreeNode node) {
        if (node instanceof DefaultMutableTreeNode) {
            final DefaultMutableTreeNode defaultNode = (DefaultMutableTreeNode) node;
            final Rectangle bounds = tree.getPathBounds(new TreePath(defaultNode.getPath()));
            // Set the height to the visible tree height to force the node to top.
            if (bounds != null) {
                bounds.height = tree.getVisibleRect().height;
                tree.scrollRectToVisible(bounds);
            }
        }
    }

    private void setSplitterProportion(float value) {
        isSettingSplitterProportion = true;
        try {
            splitter.setProportion(value);
        } finally {
            isSettingSplitterProportion = false;
        }
        doLayoutRecursively(splitter);
    }

    private static void doLayoutRecursively(Component component) {
        if (component != null) {
            component.doLayout();
            if (component instanceof Container) {
                final Container container = (Container) component;
                for (Component child : container.getComponents()) {
                    doLayoutRecursively(child);
                }
            }
        }
    }

    private void showNotRenderableInPreviewArea(@NotNull FlutterOutline widget) {
        final JPanel panel = new JPanel();
        panel.setLayout(new MigLayout("insets 0", "[grow, center]", "[grow, bottom][grow 200, top]"));

        panel.add(new JBLabel(PreviewArea.NOT_RENDERABLE), "cell 0 0");

        final LinkLabel linkLabel = LinkLabel.create("Add forDesignTime() constructor...", () -> {
            final SourceChange change = flutterAnalysisServer
                    .flutter_getChangeAddForDesignTimeConstructor(currentFile, widget.getOffset());
            if (change != null) {
                applyChangeAndShowException(change);
            }
        });
        panel.add(linkLabel, "cell 0 1");

        previewArea.clear(panel);

        minimizePreviewArea();
    }

    private void showLocalException(@NotNull Throwable localException) {
        final JPanel panel = new JPanel();
        panel.setLayout(new MigLayout("insets 0", "[grow, center]", "[grow, bottom][grow 200, top]"));

        panel.add(new JBLabel("Encountered an exception during rendering"), "cell 0 0");

        final LinkLabel linkLabel = LinkLabel.create("Show exception...", () -> {
            final StringWriter stringWriter = new StringWriter();
            final PrintWriter printWriter = new PrintWriter(stringWriter);
            printWriter.println(localException.getMessage());
            localException.printStackTrace(printWriter);
            final Module module = currentFile == null ? null : ModuleUtil.findModuleForFile(currentFile, project);
            FlutterConsoles.displayMessage(project, module, stringWriter.toString().trim(), true);
        });
        panel.add(linkLabel, "cell 0 1");

        previewArea.clear(panel);
    }

    private void showRemoteException(@NotNull JsonObject remoteException) {
        final JPanel panel = new JPanel();
        panel.setLayout(new MigLayout("insets 0", "[grow, center]", "[grow, bottom][grow 200, top]"));

        panel.add(new JBLabel("Encountered an exception during rendering"), "cell 0 0");

        final LinkLabel linkLabel = LinkLabel.create("Show exception...", () -> {
            final StringWriter stringWriter = new StringWriter();
            final PrintWriter printWriter = new PrintWriter(stringWriter);
            printWriter.println(remoteException.get("exception").getAsString());
            printWriter.println(remoteException.get("stackTrace").getAsString());
            final Module module = currentFile == null ? null : ModuleUtil.findModuleForFile(currentFile, project);
            FlutterConsoles.displayMessage(project, module, stringWriter.toString().trim(), true);
        });
        panel.add(linkLabel, "cell 0 1");

        previewArea.clear(panel);
    }

    private void applyChangeAndShowException(SourceChange change) {
        ApplicationManager.getApplication().runWriteAction(() -> {
            try {
                AssistUtils.applySourceChange(project, change, false);
            } catch (DartSourceEditException exception) {
                FlutterMessages.showError("Error applying change", exception.getMessage());
            }
        });
    }

    private void sendAnalyticEvent(@NotNull String name) {
        FlutterInitializer.getAnalytics().sendEvent("preview", name);
    }

    private class QuickAssistAction extends AnAction {
        private final String id;

        QuickAssistAction(@NotNull String id, Icon icon, String assistMessage) {
            super(assistMessage, null, icon);
            this.id = id;
            messageToActionMap.put(assistMessage, this);
        }

        @Override
        public void actionPerformed(AnActionEvent e) {
            sendAnalyticEvent(id);
            final SourceChange change;
            synchronized (actionToChangeMap) {
                change = actionToChangeMap.get(this);
                actionToChangeMap.clear();
            }
            if (change != null) {
                applyChangeAndShowException(change);
            }
        }

        @Override
        public void update(AnActionEvent e) {
            final boolean hasChange = actionToChangeMap.containsKey(this);
            e.getPresentation().setEnabled(hasChange);
        }

        boolean isEnabled() {
            return actionToChangeMap.containsKey(this);
        }
    }

    private class ExtractMethodAction extends AnAction {
        private final String id = "dart.assist.flutter.extractMethod";

        ExtractMethodAction() {
            super("Extract method...", null, FlutterIcons.ExtractMethod);
        }

        @Override
        public void actionPerformed(AnActionEvent e) {
            final AnAction action = ActionManager.getInstance().getAction("ExtractMethod");
            if (action != null) {
                final FlutterOutline outline = getWidgetOutline();
                if (outline != null) {
                    TransactionGuard.submitTransaction(project, () -> {
                        // Ideally we don't need this - just caret at the beginning should be enough.
                        // Unfortunately this was implemented only recently.
                        // So, we have to select the widget range.
                        final int offset = getConvertedOutlineOffset(outline);
                        final int end = getConvertedOutlineEnd(outline);
                        currentEditor.getSelectionModel().setSelection(offset, end);

                        final JComponent editorComponent = currentEditor.getComponent();
                        final DataContext editorContext = DataManager.getInstance().getDataContext(editorComponent);
                        final AnActionEvent editorEvent = AnActionEvent.createFromDataContext(ActionPlaces.UNKNOWN,
                                null, editorContext);

                        action.actionPerformed(editorEvent);
                    });
                }
            }
        }

        @Override
        public void update(AnActionEvent e) {
            final boolean isEnabled = isEnabled();
            e.getPresentation().setEnabled(isEnabled);
        }

        boolean isEnabled() {
            return getWidgetOutline() != null;
        }

        private FlutterOutline getWidgetOutline() {
            final List<FlutterOutline> outlines = getOutlinesSelectedInTree();
            if (outlines.size() == 1) {
                final FlutterOutline outline = outlines.get(0);
                if (outline.getDartElement() == null) {
                    return outline;
                }
            }
            return null;
        }
    }

    private class ShowOnlyWidgetsAction extends AnAction implements Toggleable, RightAlignedToolbarAction {
        ShowOnlyWidgetsAction(@NotNull Icon icon, @NotNull String text) {
            super(text, null, icon);
        }

        @Override
        public void actionPerformed(AnActionEvent e) {
            final FlutterSettings flutterSettings = FlutterSettings.getInstance();
            flutterSettings.setShowOnlyWidgets(!flutterSettings.isShowOnlyWidgets());
            if (currentOutline != null) {
                updateOutline(currentOutline);
            }
        }

        @Override
        public void update(AnActionEvent e) {
            final Presentation presentation = e.getPresentation();
            presentation.putClientProperty(SELECTED_PROPERTY, FlutterSettings.getInstance().isShowOnlyWidgets());
            presentation.setEnabled(currentOutline != null);
        }
    }
}

/**
 * We subclass {@link SimpleToolWindowPanel} to implement "getData" and return {@link PlatformDataKeys#FILE_EDITOR},
 * so that Undo/Redo actions work.
 */
class OutlineComponent extends SimpleToolWindowPanel {
    private final PreviewView myView;

    OutlineComponent(PreviewView view) {
        super(true, true);
        myView = view;
    }

    @Override
    public Object getData(String dataId) {
        if (PlatformDataKeys.FILE_EDITOR.is(dataId)) {
            return myView.currentFileEditor;
        }
        return super.getData(dataId);
    }
}

class OutlineTree extends Tree {
    OutlineTree(DefaultMutableTreeNode model) {
        super(model);

        setRootVisible(false);
        setToggleClickCount(0);
    }

    void expandAll() {
        for (int row = 0; row < getRowCount(); row++) {
            expandRow(row);
        }
    }
}

class OutlineObject {
    private static final CustomIconMaker iconMaker = new CustomIconMaker();
    private static final Map<Icon, LayeredIcon> flutterDecoratedIcons = new HashMap<>();

    final FlutterOutline outline;
    private Icon icon;

    OutlineObject(FlutterOutline outline) {
        this.outline = outline;
    }

    Icon getIcon() {
        if (outline.getKind().equals(FlutterOutlineKind.DART_ELEMENT)) {
            return null;
        }

        if (icon == null) {
            final String className = outline.getClassName();
            final FlutterWidget widget = FlutterWidget.getCatalog().getWidget(className);
            if (widget != null) {
                icon = widget.getIcon();
            }
            if (icon == null) {
                icon = iconMaker.fromWidgetName(className);
            }
        }

        return icon;
    }

    Icon getFlutterDecoratedIcon() {
        final Icon icon = getIcon();
        if (icon == null)
            return null;

        LayeredIcon decorated = flutterDecoratedIcons.get(icon);
        if (decorated == null) {
            final Icon badgeIcon = FlutterIcons.Flutter_badge;

            decorated = new LayeredIcon(2);
            decorated.setIcon(badgeIcon, 0, 0, 1 + (icon.getIconHeight() - badgeIcon.getIconHeight()) / 2);
            decorated.setIcon(icon, 1, badgeIcon.getIconWidth(), 0);

            flutterDecoratedIcons.put(icon, decorated);
        }

        return decorated;
    }

    /**
     * Return the string that is suitable for speed search. It has every name part separated so that we search only inside individual name
     * parts, but not in their accidential concatenation.
     */
    @NotNull
    String getSpeedSearchString() {
        final StringBuilder builder = new StringBuilder();
        final Element dartElement = outline.getDartElement();
        if (dartElement != null) {
            builder.append(dartElement.getName());
        } else {
            builder.append(outline.getClassName());
        }
        if (outline.getVariableName() != null) {
            builder.append('|');
            builder.append(outline.getVariableName());
        }

        final List<FlutterOutlineAttribute> attributes = outline.getAttributes();
        if (attributes != null) {
            for (FlutterOutlineAttribute attribute : attributes) {
                builder.append(attribute.getName());
                builder.append(':');
                builder.append(attribute.getLabel());
            }
        }

        return builder.toString();
    }
}

class OutlineTreeCellRenderer extends ColoredTreeCellRenderer {
    private JTree tree;
    private boolean selected;

    public void customizeCellRenderer(@NotNull final JTree tree, final Object value, final boolean selected,
            final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) {
        final Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
        if (!(userObject instanceof OutlineObject)) {
            return;
        }
        final OutlineObject node = (OutlineObject) userObject;
        final FlutterOutline outline = node.outline;

        this.tree = tree;
        this.selected = selected;

        // Render a Dart element.
        final Element dartElement = outline.getDartElement();
        if (dartElement != null) {
            final Icon icon = DartElementPresentationUtil.getIcon(dartElement);
            setIcon(icon);

            final boolean renderInBold = hasWidgetChild(outline) && ModelUtils.isBuildMethod(dartElement);
            DartElementPresentationUtil.renderElement(dartElement, this, renderInBold);
            return;
        }

        // Render the widget icon.
        final Icon icon = node.getFlutterDecoratedIcon();
        if (icon != null) {
            setIcon(icon);
        }

        // Render the widget class.
        appendSearch(outline.getClassName(), SimpleTextAttributes.REGULAR_ATTRIBUTES);

        // Render the variable.
        if (outline.getVariableName() != null) {
            append(" ");
            appendSearch(outline.getVariableName(), SimpleTextAttributes.GRAYED_ATTRIBUTES);
        }

        // Render a generic label.
        if (outline.getKind().equals(FlutterOutlineKind.GENERIC) && outline.getLabel() != null) {
            append(" ");
            final String label = outline.getLabel();
            appendSearch(label, SimpleTextAttributes.GRAYED_ATTRIBUTES);
        }

        // Append all attributes.
        final List<FlutterOutlineAttribute> attributes = outline.getAttributes();
        if (attributes != null) {
            if (attributes.size() == 1 && isAttributeElidable(attributes.get(0).getName())) {
                final FlutterOutlineAttribute attribute = attributes.get(0);
                append(" ");
                appendSearch(attribute.getLabel(), SimpleTextAttributes.GRAYED_ATTRIBUTES);
            } else {
                for (int i = 0; i < attributes.size(); i++) {
                    final FlutterOutlineAttribute attribute = attributes.get(i);

                    if (i > 0) {
                        append(",");
                    }
                    append(" ");

                    if (!StringUtil.equals("data", attribute.getName())) {
                        appendSearch(attribute.getName(), SimpleTextAttributes.GRAYED_ATTRIBUTES);
                        append(": ", SimpleTextAttributes.GRAYED_ATTRIBUTES);
                    }

                    appendSearch(attribute.getLabel(), SimpleTextAttributes.GRAYED_ATTRIBUTES);

                    // TODO(scheglov): custom display for units, colors, iterables, and icons?
                }
            }
        }
    }

    private boolean hasWidgetChild(FlutterOutline outline) {
        if (outline.getChildren() == null) {
            return false;
        }

        for (FlutterOutline child : outline.getChildren()) {
            if (child.getDartElement() == null) {
                return true;
            }
        }

        return false;
    }

    private static boolean isAttributeElidable(String name) {
        return StringUtil.equals("text", name) || StringUtil.equals("icon", name);
    }

    void appendSearch(@NotNull String text, @NotNull SimpleTextAttributes attributes) {
        SpeedSearchUtil.appendFragmentsForSpeedSearch(tree, text, attributes, selected, this);
    }
}

/**
 * Delegate to the given action, but do not render the action's icon.
 */
class TextOnlyActionWrapper extends AnAction {
    private final AnAction action;

    public TextOnlyActionWrapper(AnAction action) {
        super(action.getTemplatePresentation().getText());

        this.action = action;
    }

    @Override
    public void actionPerformed(AnActionEvent event) {
        action.actionPerformed(event);
    }
}