/* * Copyright 2000-2013 JetBrains s.r.o. * * 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.intellij.ide.bookmarks; import com.intellij.codeInsight.daemon.GutterMark; import com.intellij.icons.AllIcons; import com.intellij.ide.IdeBundle; import com.intellij.ide.structureView.StructureViewBuilder; import com.intellij.ide.structureView.StructureViewModel; import com.intellij.ide.structureView.TreeBasedStructureViewBuilder; import com.intellij.lang.LanguageStructureViewBuilder; import com.intellij.navigation.ItemPresentation; import com.intellij.navigation.NavigationItem; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.RangeMarker; import com.intellij.openapi.editor.colors.CodeInsightColors; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.ex.MarkupModelEx; import com.intellij.openapi.editor.ex.RangeHighlighterEx; import com.intellij.openapi.editor.impl.DocumentMarkupModel; import com.intellij.openapi.editor.markup.GutterIconRenderer; import com.intellij.openapi.editor.markup.HighlighterLayer; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.pom.Navigatable; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.ui.JBColor; import com.intellij.util.Processor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; public class Bookmark implements Navigatable { private static final JBColor ICON_BACKGROUND_COLOR = new JBColor(new Color(0xffffcc), new Color(0x675133)); public static final Icon DEFAULT_ICON = new MyDefaultIcon(); private final VirtualFile myFile; @NotNull private final OpenFileDescriptor myTarget; private final Project myProject; private String myDescription; private char myMnemonic = 0; public static final Font MNEMONIC_FONT = new Font("Monospaced", 0, 11); public Bookmark(@NotNull Project project, @NotNull VirtualFile file, int line, @NotNull String description) { myFile = file; myProject = project; myDescription = description; myTarget = new OpenFileDescriptor(project, file, line, -1, true); addHighlighter(); } public void updateHighlighter() { release(); addHighlighter(); } private void addHighlighter() { Document document = FileDocumentManager.getInstance().getCachedDocument(getFile()); if (document != null) { createHighlighter((MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true)); } } public RangeHighlighter createHighlighter(@NotNull MarkupModelEx markup) { final RangeHighlighterEx myHighlighter; int line = getLine(); if (line >= 0) { myHighlighter = markup.addPersistentLineHighlighter(line, HighlighterLayer.ERROR + 1, null); if (myHighlighter != null) { myHighlighter.setGutterIconRenderer(new MyGutterIconRenderer(this)); TextAttributes textAttributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.BOOKMARKS_ATTRIBUTES); Color stripeColor = textAttributes.getErrorStripeColor(); myHighlighter.setErrorStripeMarkColor(stripeColor != null ? stripeColor : Color.black); myHighlighter.setErrorStripeTooltip(getBookmarkTooltip()); TextAttributes attributes = myHighlighter.getTextAttributes(); if (attributes == null) { attributes = new TextAttributes(); } attributes.setBackgroundColor(textAttributes.getBackgroundColor()); attributes.setForegroundColor(textAttributes.getForegroundColor()); myHighlighter.setTextAttributes(attributes); } } else { myHighlighter = null; } return myHighlighter; } public Document getDocument() { return FileDocumentManager.getInstance().getDocument(getFile()); } public void release() { int line = getLine(); if (line < 0) { return; } final Document document = getDocument(); if (document == null) return; MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true); final Document markupDocument = markup.getDocument(); if (markupDocument.getLineCount() <= line) return; final int startOffset = markupDocument.getLineStartOffset(line); final int endOffset = markupDocument.getLineEndOffset(line); final Ref<RangeHighlighterEx> found = new Ref<RangeHighlighterEx>(); markup.processRangeHighlightersOverlappingWith(startOffset, endOffset, new Processor<RangeHighlighterEx>() { @Override public boolean process(RangeHighlighterEx highlighter) { GutterMark renderer = highlighter.getGutterIconRenderer(); if (renderer instanceof MyGutterIconRenderer && ((MyGutterIconRenderer)renderer).myBookmark == Bookmark.this) { found.set(highlighter); return false; } return true; } }); if (!found.isNull()) found.get().dispose(); } public Icon getIcon() { return myMnemonic == 0 ? DEFAULT_ICON : MnemonicIcon.getIcon(myMnemonic); } public String getDescription() { return myDescription; } public void setDescription(String description) { myDescription = description; } public char getMnemonic() { return myMnemonic; } public void setMnemonic(char mnemonic) { myMnemonic = Character.toUpperCase(mnemonic); } @NotNull public VirtualFile getFile() { return myFile; } @Nullable public String getNotEmptyDescription() { return StringUtil.isEmpty(myDescription) ? null : myDescription; } public boolean isValid() { if (!getFile().isValid()) { return false; } // There is a possible case that target document line that is referenced by the current bookmark is removed. We assume // that corresponding range marker becomes invalid then. RangeMarker rangeMarker = myTarget.getRangeMarker(); return rangeMarker == null || rangeMarker.isValid(); } @Override public boolean canNavigate() { return myTarget.canNavigate(); } @Override public boolean canNavigateToSource() { return myTarget.canNavigateToSource(); } @Override public void navigate(boolean requestFocus) { myTarget.navigate(requestFocus); } public int getLine() { RangeMarker marker = myTarget.getRangeMarker(); if (marker != null && marker.isValid()) { Document document = marker.getDocument(); return document.getLineNumber(marker.getStartOffset()); } return myTarget.getLine(); } @Override public String toString() { StringBuilder result = new StringBuilder(getQualifiedName()); String description = StringUtil.escapeXml(getNotEmptyDescription()); if (description != null) { result.append(": ").append(description); } return result.toString(); } public String getQualifiedName() { String presentableUrl = myFile.getPresentableUrl(); if (myFile.isDirectory()) return presentableUrl; PsiDocumentManager.getInstance(myProject).commitAllDocuments(); final PsiFile psiFile = PsiManager.getInstance(myProject).findFile(myFile); if (psiFile == null) return presentableUrl; StructureViewBuilder builder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(psiFile); if (builder instanceof TreeBasedStructureViewBuilder) { StructureViewModel model = ((TreeBasedStructureViewBuilder)builder).createStructureViewModel(null); Object element; try { element = model.getCurrentEditorElement(); } finally { model.dispose(); } if (element instanceof NavigationItem) { ItemPresentation presentation = ((NavigationItem)element).getPresentation(); if (presentation != null) { presentableUrl = ((NavigationItem)element).getName() + " " + presentation.getLocationString(); } } } return IdeBundle.message("bookmark.file.X.line.Y", presentableUrl, getLine() + 1); } private String getBookmarkTooltip() { StringBuilder result = new StringBuilder("Bookmark"); if (myMnemonic != 0) { result.append(" ").append(myMnemonic); } String description = StringUtil.escapeXml(getNotEmptyDescription()); if (description != null) { result.append(": ").append(description); } return result.toString(); } static class MnemonicIcon implements Icon { private static final MnemonicIcon[] cache = new MnemonicIcon[36];//0..9 + A..Z private final char myMnemonic; @NotNull static MnemonicIcon getIcon(char mnemonic) { int index = mnemonic - 48; if (index > 9) index -= 7; if (index < 0 || index > cache.length-1) return new MnemonicIcon(mnemonic); if (cache[index] == null) cache[index] = new MnemonicIcon(mnemonic); return cache[index]; } private MnemonicIcon(char mnemonic) { myMnemonic = mnemonic; } @Override public void paintIcon(Component c, Graphics g, int x, int y) { g.setColor(ICON_BACKGROUND_COLOR); g.fillRect(x, y, getIconWidth(), getIconHeight()); g.setColor(JBColor.GRAY); g.drawRect(x, y, getIconWidth(), getIconHeight()); g.setColor(JBColor.foreground()); final Font oldFont = g.getFont(); g.setFont(MNEMONIC_FONT); ((Graphics2D)g).drawString(Character.toString(myMnemonic), x + 3, y + getIconHeight() - 1.5F); g.setFont(oldFont); } @Override public int getIconWidth() { return DEFAULT_ICON.getIconWidth(); } @Override public int getIconHeight() { return DEFAULT_ICON.getIconHeight(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MnemonicIcon that = (MnemonicIcon)o; return myMnemonic == that.myMnemonic; } @Override public int hashCode() { return (int)myMnemonic; } } private static class MyDefaultIcon implements Icon { private static final Icon myIcon = AllIcons.Actions.Checked; @Override public void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2 = (Graphics2D)g.create(); try { Color gutterBackground = EditorColors.GUTTER_BACKGROUND.getDefaultColor(); g2.setColor(gutterBackground); g2.fillRoundRect(x, y, getIconWidth(), getIconHeight(), 4, 4); myIcon.paintIcon(c, g2, x, y); } finally { g2.dispose(); } } @Override public int getIconWidth() { return myIcon.getIconWidth(); } @Override public int getIconHeight() { return myIcon.getIconHeight(); } } private static class MyGutterIconRenderer extends GutterIconRenderer { private final Bookmark myBookmark; public MyGutterIconRenderer(@NotNull Bookmark bookmark) { myBookmark = bookmark; } @Override @NotNull public Icon getIcon() { return myBookmark.getIcon(); } @Override public String getTooltipText() { return myBookmark.getBookmarkTooltip(); } @Override public boolean equals(Object obj) { return obj instanceof MyGutterIconRenderer && Comparing.equal(getTooltipText(), ((MyGutterIconRenderer)obj).getTooltipText()) && Comparing.equal(getIcon(), ((MyGutterIconRenderer)obj).getIcon()); } @Override public int hashCode() { return getIcon().hashCode(); } } }