com.android.tools.idea.gradle.editor.ui.ReferencedValuesGradleEditorComponent.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.gradle.editor.ui.ReferencedValuesGradleEditorComponent.java

Source

/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.tools.idea.gradle.editor.ui;

import com.android.SdkConstants;
import com.android.tools.idea.gradle.editor.entity.GradleEditorSourceBinding;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.*;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.BalloonBuilder;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.JBColor;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBPanel;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.GridBag;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.image.BufferedImage;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

/**
 * There is a possible case that there is more than one {@link GradleEditorSourceBinding source binding} for particular value, e.g.:
 * <pre>
 *   ext.COMPILE_SDK_VERSION = 1
 *   if (System.getenv("my-custom-environment)) {
 *     COMPILE_SDK_VERSION = 2
 *   }
 * </pre>
 * Here there are two bindings - <code>'1'</code> and <code>'2'</code>, so, we should somehow indicate at UI level that there
 * is no single value and provide convenient access to the registered bindings.
 * <p/>
 * Current control solves that task.
 */
public class ReferencedValuesGradleEditorComponent extends JBPanel {

    private static final Function<GradleEditorSourceBinding, VirtualFile> GROUPER = new Function<GradleEditorSourceBinding, VirtualFile>() {
        @Override
        public VirtualFile apply(GradleEditorSourceBinding input) {
            return input.getFile();
        }
    };

    private static final Comparator<VirtualFile> FILES_COMPARATOR = new Comparator<VirtualFile>() {
        @Override
        public int compare(VirtualFile f1, VirtualFile f2) {
            if (f1.equals(f2)) {
                return 0;
            }

            VirtualFile d1 = f1.isDirectory() ? f1 : f1.getParent();
            VirtualFile d2 = f2.isDirectory() ? f2 : f2.getParent();
            if (d1.equals(d2)) {
                // Just use lexicographic order for files from the same directory.
                return f1.getName().compareTo(f2.getName());
            }

            // The general idea is to prefer files located more close to the file system root to files located lower.
            if (VfsUtilCore.isAncestor(d1, d2, false)) {
                return -1;
            } else if (VfsUtilCore.isAncestor(d2, d1, false)) {
                return 1;
            }
            for (VirtualFile p1 = d1.getParent(), p2 = d2.getParent();; p1 = p1.getParent(), p2 = p2.getParent()) {
                if (p1 == null && p2 == null) {
                    return 0;
                } else if (p1 == null) {
                    return -1;
                } else if (p2 == null) {
                    return 1;
                }
            }
        }
    };

    private static final Comparator<RangeMarker> RANGE_COMPARATOR = new Comparator<RangeMarker>() {
        @Override
        public int compare(RangeMarker rm1, RangeMarker rm2) {
            if (rm1.getStartOffset() < rm2.getStartOffset()) {
                return -1;
            } else if (rm2.getStartOffset() < rm1.getStartOffset()) {
                return 1;
            } else if (rm1.getEndOffset() < rm2.getEndOffset()) {
                return -1;
            } else if (rm2.getEndOffset() < rm1.getEndOffset()) {
                return 1;
            }
            return 0;
        }
    };

    /** Holds source binding grouped by file */
    private final Map<String, List<RangeMarker>> mySourceBindings = Maps.newLinkedHashMap();
    private final Map<String, VirtualFile> myFilesByName = Maps.newHashMap();
    @Nullable
    private WeakReference<Project> myProjectRef;

    public ReferencedValuesGradleEditorComponent() {
        super(new GridBagLayout());
        final JBLabel label = new JBLabel("<~>");
        label.setCursor(new Cursor(Cursor.HAND_CURSOR));
        setBackground(GradleEditorUiConstants.BACKGROUND_COLOR);
        TextAttributes attributes = EditorColorsManager.getInstance().getGlobalScheme()
                .getAttributes(EditorColors.FOLDED_TEXT_ATTRIBUTES);
        if (attributes != null) {
            Color color = attributes.getForegroundColor();
            if (color != null) {
                label.setForeground(color);
            }
        }
        add(label, new GridBag());
        label.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseReleased(MouseEvent e) {
                WeakReference<Project> projectRef = myProjectRef;
                if (projectRef == null) {
                    return;
                }
                Project project = projectRef.get();
                if (project == null) {
                    return;
                }
                final Ref<Balloon> balloonRef = new Ref<Balloon>();
                Content content = new Content(project, new Runnable() {
                    @Override
                    public void run() {
                        Balloon balloon = balloonRef.get();
                        if (balloon != null && !balloon.isDisposed()) {
                            Disposer.dispose(balloon);
                            balloonRef.set(null);
                        }
                    }
                });
                BalloonBuilder builder = JBPopupFactory.getInstance().createBalloonBuilder(content)
                        .setDisposable(project).setShowCallout(false)
                        .setAnimationCycle(GradleEditorUiConstants.ANIMATION_TIME_MILLIS)
                        .setFillColor(JBColor.border());
                Balloon balloon = builder.createBalloon();
                balloonRef.set(balloon);
                balloon.show(new RelativePoint(label, new Point(label.getWidth() / 2, label.getHeight())),
                        Balloon.Position.atRight);
            }
        });
    }

    public void bind(@NotNull Project project, @NotNull List<GradleEditorSourceBinding> sourceBindings) {
        myProjectRef = new WeakReference<Project>(project);
        ImmutableListMultimap<VirtualFile, GradleEditorSourceBinding> byFile = Multimaps.index(sourceBindings,
                GROUPER);
        List<VirtualFile> orderedFiles = Lists.newArrayList(byFile.keySet());
        ContainerUtil.sort(orderedFiles, FILES_COMPARATOR);
        for (VirtualFile file : orderedFiles) {
            ImmutableList<GradleEditorSourceBinding> list = byFile.get(file);
            List<RangeMarker> rangeMarkers = Lists.newArrayList();
            for (GradleEditorSourceBinding descriptor : list) {
                rangeMarkers.add(descriptor.getRangeMarker());
            }
            if (!rangeMarkers.isEmpty()) {
                ContainerUtil.sort(rangeMarkers, RANGE_COMPARATOR);
                String name = getRepresentativeName(project, file);
                mySourceBindings.put(name, rangeMarkers);
                myFilesByName.put(name, file);
            }
        }
    }

    /**
     * @param project  current ide project
     * @param file     target file
     * @return         convenient user-readable name for the given file, e.g. there is a possible case that we have a multi-project
     *                 and multiple {@code build.gradle} files are located there. We want to show names like {@code build.gradle},
     *                 {@code :app:build.gradle} instead of full paths then
     */
    @NotNull
    private static String getRepresentativeName(@NotNull Project project, @NotNull VirtualFile file) {
        VirtualFile projectBaseDir = project.getBaseDir();
        if (!VfsUtilCore.isAncestor(projectBaseDir, file, false)) {
            return file.getPresentableName();
        }
        List<String> pathEntries = Lists.newArrayList();
        for (VirtualFile f = file.getParent(); !projectBaseDir.equals(f); f = f.getParent()) {
            pathEntries.add(f.getPresentableName());
        }
        if (pathEntries.isEmpty()) {
            return file.getPresentableName();
        }
        Collections.reverse(pathEntries);
        String sep = SdkConstants.GRADLE_PATH_SEPARATOR;
        return sep + Joiner.on(sep).join(pathEntries) + sep + file.getPresentableName();
    }

    /**
     * Creates an image which represents editor's text from the region identified by the given range marker.
     *
     * @param editor      an editor which serves as a renderer for the target text
     * @param marker      range marker that points to the target text region
     * @param minWidthPx  width in pixels to constraint resulting image from below
     * @return            an image which represents target text fragment
     */
    @NotNull
    private static BufferedImage getContentToShow(@NotNull Editor editor, @NotNull RangeMarker marker,
            int minWidthPx) {
        int maxWidth = Toolkit.getDefaultToolkit().getScreenSize().width * 4 / 5;
        Document document = editor.getDocument();
        int startLine = document.getLineNumber(marker.getStartOffset());
        int endLine = document.getLineNumber(marker.getEndOffset());
        int minStartX = Integer.MAX_VALUE;
        int maxEndX = 0;
        CharSequence text = document.getCharsSequence();

        // Calculate desired text dimensions.
        for (int line = startLine; line <= endLine; line++) {
            int startOffsetToUse = CharArrayUtil.shiftForward(text, document.getLineStartOffset(line),
                    document.getLineEndOffset(line), " \t");
            int endOffsetToUse = CharArrayUtil.shiftBackward(text, document.getLineStartOffset(line),
                    document.getLineEndOffset(line), " \t");
            minStartX = Math.min(minStartX, offsetToXY(editor, startOffsetToUse).x);
            maxEndX = Math.max(maxEndX, offsetToXY(editor, endOffsetToUse).x);
        }

        // Calculate text dimensions taking into consideration min/max/desired width
        int desiredWidth = maxEndX - minStartX;
        final int xStart;
        final int xEnd;
        if (desiredWidth > maxWidth) {
            int xShift = (desiredWidth - maxWidth) / 2;
            xStart = offsetToXY(editor, minStartX).x + xShift;
            xEnd = xStart + maxWidth;
        } else if (desiredWidth < minWidthPx) {
            int xShift = (minWidthPx - desiredWidth) / 2;
            xStart = offsetToXY(editor, minStartX).x - xShift;
            xEnd = xStart + minWidthPx;
        } else {
            xStart = minStartX;
            xEnd = maxEndX;
        }
        int lineHeight = editor.getLineHeight();
        int yStart = offsetToXY(editor, marker.getStartOffset()).y;
        int yEnd = yStart + lineHeight + lineHeight * (endLine - startLine);

        int width = xEnd - xStart;
        int height = yEnd - yStart;

        // Ask the editor to render target text.
        JScrollPane scrollPane = UIUtil.findComponentOfType(editor.getComponent(), JScrollPane.class);
        BufferedImage image = UIUtil.createImage(width, height, BufferedImage.TYPE_INT_ARGB);
        if (scrollPane != null) {
            Component editorComponent = scrollPane.getViewport().getView();
            editorComponent.setSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
            Graphics2D graphics = image.createGraphics();
            UISettings.setupAntialiasing(graphics);
            graphics.translate(-xStart, -yStart);
            graphics.setClip(xStart, yStart, width, height);
            editorComponent.paint(graphics);
            graphics.dispose();
        }

        return image;
    }

    @NotNull
    private static Point offsetToXY(@NotNull Editor editor, int offset) {
        return editor.visualPositionToXY(editor.offsetToVisualPosition(offset));
    }

    private class Content extends JBPanel {

        private static final String FILE_KEY = "__FILE";
        private static final String MARKER_KEY = "__MARKER";

        private final List<JComponent> myTextFragmentPanels = Lists.newArrayList();
        @NotNull
        private final Runnable myCloseCallback;
        @Nullable
        private JComponent myTextFragmentPanelUnderMouse;

        /**
         * Constructs new <code>ReferencedValuesGradleEditorComponent</code> object.
         *
         * @param project        current ide project
         * @param closeCallback  callback to notify that current content should be closed
         */
        Content(@NotNull final Project project, @NotNull Runnable closeCallback) {
            super(new GridBagLayout());
            myCloseCallback = closeCallback;
            setBackground(GradleEditorUiConstants.BACKGROUND_COLOR);
            int gap = 8;
            GridBag constraints = new GridBag().fillCellHorizontally().weightx(1).coverLine().insets(gap, gap, gap,
                    gap);
            EditorFactory editorFactory = EditorFactory.getInstance();
            int maxTitleWidthPx = 0;
            for (String s : mySourceBindings.keySet()) {
                maxTitleWidthPx = Math.max(maxTitleWidthPx,
                        getFontMetrics(UIUtil.getTitledBorderFont()).stringWidth(s));
            }
            for (Map.Entry<String, List<RangeMarker>> entry : mySourceBindings.entrySet()) {
                JBPanel titledPanel = new JBPanel(new GridBagLayout());
                titledPanel.setBackground(GradleEditorUiConstants.BACKGROUND_COLOR);
                titledPanel.setBorder(IdeBorderFactory.createTitledBorder(entry.getKey()));
                boolean hasContent = false;
                Editor editor = null;
                for (RangeMarker marker : entry.getValue()) {
                    if (!marker.isValid()) {
                        continue;
                    }
                    if (editor == null) {
                        editor = editorFactory.createEditor(marker.getDocument(), project);
                    }
                    JBPanel fragmentPanel = new JBPanel(new GridBagLayout());
                    fragmentPanel.putClientProperty(FILE_KEY, entry.getKey());
                    fragmentPanel.putClientProperty(MARKER_KEY, marker);
                    fragmentPanel.setForeground(GradleEditorUiConstants.BACKGROUND_COLOR);
                    fragmentPanel.setBackground(GradleEditorUiConstants.BACKGROUND_COLOR);
                    fragmentPanel.setBorder(BorderFactory.createLoweredBevelBorder());
                    final BufferedImage contentToShow = getContentToShow(editor, marker, maxTitleWidthPx);
                    fragmentPanel.add(new JLabel(new ImageIcon(contentToShow)), constraints);
                    hasContent = true;
                    titledPanel.add(fragmentPanel, constraints);
                    myTextFragmentPanels.add(fragmentPanel);
                }
                if (editor != null) {
                    editorFactory.releaseEditor(editor);
                }
                if (hasContent) {
                    add(titledPanel, constraints);
                }
            }

            addMouseMotionListener(new MouseMotionAdapter() {
                @Override
                public void mouseMoved(MouseEvent e) {
                    if (myTextFragmentPanelUnderMouse != null && !isInside(e, myTextFragmentPanelUnderMouse)) {
                        myTextFragmentPanelUnderMouse.setBorder(BorderFactory.createLoweredBevelBorder());
                        myTextFragmentPanelUnderMouse = null;
                    }

                    //noinspection NullableProblems
                    for (JComponent panel : myTextFragmentPanels) {
                        if (isInside(e, panel)) {
                            myTextFragmentPanelUnderMouse = panel;
                            panel.setBorder(BorderFactory.createRaisedBevelBorder());
                        }
                    }
                }
            });

            addMouseListener(new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    if (myTextFragmentPanelUnderMouse != null && isInside(e, myTextFragmentPanelUnderMouse)) {
                        myTextFragmentPanelUnderMouse.setBorder(BorderFactory.createLoweredBevelBorder());
                    }
                }

                @Override
                public void mouseClicked(MouseEvent e) {
                    if (myTextFragmentPanelUnderMouse == null || !isInside(e, myTextFragmentPanelUnderMouse)) {
                        return;
                    }
                    Object fileName = myTextFragmentPanelUnderMouse.getClientProperty(FILE_KEY);
                    if (!(fileName instanceof String)) {
                        return;
                    }
                    VirtualFile file = myFilesByName.get(fileName.toString());
                    if (file == null) {
                        return;
                    }
                    Object m = myTextFragmentPanelUnderMouse.getClientProperty(MARKER_KEY);
                    if (!(m instanceof RangeMarker)) {
                        return;
                    }
                    RangeMarker marker = (RangeMarker) m;
                    if (!marker.isValid()) {
                        return;
                    }
                    OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, marker.getStartOffset());
                    if (descriptor.canNavigate()) {
                        descriptor.navigate(true);
                        myCloseCallback.run();
                    }
                }

                @Override
                public void mouseReleased(MouseEvent e) {
                    if (myTextFragmentPanelUnderMouse != null && isInside(e, myTextFragmentPanelUnderMouse)) {
                        myTextFragmentPanelUnderMouse.setBorder(BorderFactory.createRaisedBevelBorder());
                    }
                }
            });
        }

        private boolean isInside(@NotNull MouseEvent e, @NotNull JComponent component) {
            return component.contains(SwingUtilities.convertPoint(this, e.getPoint(), component));
        }
    }
}