| /* |
| * 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)); |
| } |
| } |
| } |