Bookmarks "adaptive" icons: post-review improvement
[idea/community.git] / platform / lang-impl / src / com / intellij / ide / bookmarks / Bookmark.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.ide.bookmarks;
18
19 import com.intellij.codeInsight.daemon.GutterMark;
20 import com.intellij.icons.AllIcons;
21 import com.intellij.ide.IdeBundle;
22 import com.intellij.ide.structureView.StructureViewBuilder;
23 import com.intellij.ide.structureView.StructureViewModel;
24 import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
25 import com.intellij.lang.LanguageStructureViewBuilder;
26 import com.intellij.navigation.ItemPresentation;
27 import com.intellij.navigation.NavigationItem;
28 import com.intellij.openapi.editor.Document;
29 import com.intellij.openapi.editor.RangeMarker;
30 import com.intellij.openapi.editor.colors.CodeInsightColors;
31 import com.intellij.openapi.editor.colors.EditorColors;
32 import com.intellij.openapi.editor.colors.EditorColorsManager;
33 import com.intellij.openapi.editor.ex.MarkupModelEx;
34 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
35 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
36 import com.intellij.openapi.editor.markup.GutterIconRenderer;
37 import com.intellij.openapi.editor.markup.HighlighterLayer;
38 import com.intellij.openapi.editor.markup.RangeHighlighter;
39 import com.intellij.openapi.editor.markup.TextAttributes;
40 import com.intellij.openapi.fileEditor.FileDocumentManager;
41 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
42 import com.intellij.openapi.project.Project;
43 import com.intellij.openapi.util.Comparing;
44 import com.intellij.openapi.util.Ref;
45 import com.intellij.openapi.util.text.StringUtil;
46 import com.intellij.openapi.vfs.VirtualFile;
47 import com.intellij.pom.Navigatable;
48 import com.intellij.psi.PsiDocumentManager;
49 import com.intellij.psi.PsiFile;
50 import com.intellij.psi.PsiManager;
51 import com.intellij.ui.ColorUtil;
52 import com.intellij.ui.JBColor;
53 import com.intellij.util.NotNullProducer;
54 import com.intellij.util.PlatformIcons;
55 import com.intellij.util.Processor;
56 import org.jetbrains.annotations.NotNull;
57 import org.jetbrains.annotations.Nullable;
58
59 import javax.swing.*;
60 import java.awt.*;
61
62 public class Bookmark implements Navigatable {
63   public static final Icon DEFAULT_ICON = new MyCheckedIcon();
64
65   private final VirtualFile myFile;
66   @NotNull private final OpenFileDescriptor myTarget;
67   private final Project myProject;
68
69   private String myDescription;
70   private char myMnemonic = 0;
71   public static final Font MNEMONIC_FONT = new Font("Monospaced", 0, 11);
72
73   public Bookmark(@NotNull Project project, @NotNull VirtualFile file, int line, @NotNull String description) {
74     myFile = file;
75     myProject = project;
76     myDescription = description;
77
78     myTarget = new OpenFileDescriptor(project, file, line, -1, true);
79
80     addHighlighter();
81   }
82
83   public void updateHighlighter() {
84     release();
85     addHighlighter();
86   }
87
88   private void addHighlighter() {
89     Document document = FileDocumentManager.getInstance().getCachedDocument(getFile());
90     if (document != null) {
91       createHighlighter((MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true));
92     }
93   }
94
95   public RangeHighlighter createHighlighter(@NotNull MarkupModelEx markup) {
96     final RangeHighlighterEx myHighlighter;
97     int line = getLine();
98     if (line >= 0) {
99       myHighlighter = markup.addPersistentLineHighlighter(line, HighlighterLayer.ERROR + 1, null);
100       if (myHighlighter != null) {
101         myHighlighter.setGutterIconRenderer(new MyGutterIconRenderer(this));
102
103         TextAttributes textAttributes =
104           EditorColorsManager.getInstance().getGlobalScheme().getAttributes(CodeInsightColors.BOOKMARKS_ATTRIBUTES);
105
106         Color stripeColor = textAttributes.getErrorStripeColor();
107         myHighlighter.setErrorStripeMarkColor(stripeColor != null ? stripeColor : Color.black);
108         myHighlighter.setErrorStripeTooltip(getBookmarkTooltip());
109
110         TextAttributes attributes = myHighlighter.getTextAttributes();
111         if (attributes == null) {
112           attributes = new TextAttributes();
113         }
114         attributes.setBackgroundColor(textAttributes.getBackgroundColor());
115         attributes.setForegroundColor(textAttributes.getForegroundColor());
116         myHighlighter.setTextAttributes(attributes);
117       }
118     }
119     else {
120       myHighlighter = null;
121     }
122     return myHighlighter;
123   }
124
125   @Nullable
126   public Document getDocument() {
127     return FileDocumentManager.getInstance().getCachedDocument(getFile());
128   }
129
130   public void release() {
131     int line = getLine();
132     if (line < 0) {
133       return;
134     }
135     final Document document = getDocument();
136     if (document == null) return;
137     MarkupModelEx markup = (MarkupModelEx)DocumentMarkupModel.forDocument(document, myProject, true);
138     final Document markupDocument = markup.getDocument();
139     if (markupDocument.getLineCount() <= line) return;
140     final int startOffset = markupDocument.getLineStartOffset(line);
141     final int endOffset = markupDocument.getLineEndOffset(line);
142
143     final Ref<RangeHighlighterEx> found = new Ref<RangeHighlighterEx>();
144     markup.processRangeHighlightersOverlappingWith(startOffset, endOffset, new Processor<RangeHighlighterEx>() {
145       @Override
146       public boolean process(RangeHighlighterEx highlighter) {
147         GutterMark renderer = highlighter.getGutterIconRenderer();
148         if (renderer instanceof MyGutterIconRenderer && ((MyGutterIconRenderer)renderer).myBookmark == Bookmark.this) {
149           found.set(highlighter);
150           return false;
151         }
152         return true;
153       }
154     });
155     if (!found.isNull()) found.get().dispose();
156   }
157
158   public Icon getIcon() {
159     return myMnemonic == 0 ? DEFAULT_ICON : MnemonicIcon.getIcon(myMnemonic);
160   }
161
162   public String getDescription() {
163     return myDescription;
164   }
165
166   public void setDescription(String description) {
167     myDescription = description;
168   }
169
170   public char getMnemonic() {
171     return myMnemonic;
172   }
173
174   public void setMnemonic(char mnemonic) {
175     myMnemonic = Character.toUpperCase(mnemonic);
176   }
177
178   @NotNull
179   public VirtualFile getFile() {
180     return myFile;
181   }
182
183   @Nullable
184   public String getNotEmptyDescription() {
185     return StringUtil.isEmpty(myDescription) ? null : myDescription;
186   }
187
188   public boolean isValid() {
189     if (!getFile().isValid()) {
190       return false;
191     }
192
193     // There is a possible case that target document line that is referenced by the current bookmark is removed. We assume
194     // that corresponding range marker becomes invalid then.
195     RangeMarker rangeMarker = myTarget.getRangeMarker();
196     return rangeMarker == null || rangeMarker.isValid();
197   }
198
199   @Override
200   public boolean canNavigate() {
201     return myTarget.canNavigate();
202   }
203
204   @Override
205   public boolean canNavigateToSource() {
206     return myTarget.canNavigateToSource();
207   }
208
209   @Override
210   public void navigate(boolean requestFocus) {
211     myTarget.navigate(requestFocus);
212   }
213
214   public int getLine() {
215     RangeMarker marker = myTarget.getRangeMarker();
216     if (marker != null && marker.isValid()) {
217       Document document = marker.getDocument();
218       return document.getLineNumber(marker.getStartOffset());
219     }
220     return myTarget.getLine();
221   }
222
223   @Override
224   public String toString() {
225     StringBuilder result = new StringBuilder(getQualifiedName());
226     String description = StringUtil.escapeXml(getNotEmptyDescription());
227     if (description != null) {
228       result.append(": ").append(description);
229     }
230     return result.toString();
231   }
232
233   public String getQualifiedName() {
234     String presentableUrl = myFile.getPresentableUrl();
235     if (myFile.isDirectory()) return presentableUrl;
236
237     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
238     final PsiFile psiFile = PsiManager.getInstance(myProject).findFile(myFile);
239
240     if (psiFile == null) return presentableUrl;
241
242     StructureViewBuilder builder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(psiFile);
243     if (builder instanceof TreeBasedStructureViewBuilder) {
244       StructureViewModel model = ((TreeBasedStructureViewBuilder)builder).createStructureViewModel(null);
245       Object element;
246       try {
247         element = model.getCurrentEditorElement();
248       }
249       finally {
250         model.dispose();
251       }
252       if (element instanceof NavigationItem) {
253         ItemPresentation presentation = ((NavigationItem)element).getPresentation();
254         if (presentation != null) {
255           presentableUrl = ((NavigationItem)element).getName() + " " + presentation.getLocationString();
256         }
257       }
258     }
259
260     return IdeBundle.message("bookmark.file.X.line.Y", presentableUrl, getLine() + 1);
261   }
262
263   private String getBookmarkTooltip() {
264     StringBuilder result = new StringBuilder("Bookmark");
265     if (myMnemonic != 0) {
266       result.append(" ").append(myMnemonic);
267     }
268     String description = StringUtil.escapeXml(getNotEmptyDescription());
269     if (description != null) {
270       result.append(": ").append(description);
271     }
272     return result.toString();
273   }
274
275   static class MnemonicIcon implements Icon {
276     private static final MnemonicIcon[] cache = new MnemonicIcon[36];//0..9  + A..Z
277     private final char myMnemonic;
278
279     @NotNull
280     static MnemonicIcon getIcon(char mnemonic) {
281       int index = mnemonic - 48;
282       if (index > 9)
283         index -= 7;
284       if (index < 0 || index > cache.length-1)
285         return new MnemonicIcon(mnemonic);
286       if (cache[index] == null)
287         cache[index] = new MnemonicIcon(mnemonic);
288       return cache[index];
289     }
290
291     private MnemonicIcon(char mnemonic) {
292       myMnemonic = mnemonic;
293     }
294
295     @Override
296     public void paintIcon(Component c, Graphics g, int x, int y) {
297       g.setColor(new JBColor(new NotNullProducer<Color>() {
298         @NotNull
299         @Override
300         public Color produce() {
301           //noinspection UseJBColor
302           return !darkBackground() ? new Color(0xffffcc) : new Color(0x675133);
303         }
304       }));
305       g.fillRect(x, y, getIconWidth(), getIconHeight());
306
307       g.setColor(JBColor.GRAY);
308       g.drawRect(x, y, getIconWidth(), getIconHeight());
309
310       g.setColor(EditorColorsManager.getInstance().getGlobalScheme().getDefaultForeground());
311       final Font oldFont = g.getFont();
312       g.setFont(MNEMONIC_FONT);
313
314       ((Graphics2D)g).drawString(Character.toString(myMnemonic), x + 3, y + getIconHeight() - 1.5F);
315       g.setFont(oldFont);
316     }
317
318     @Override
319     public int getIconWidth() {
320       return DEFAULT_ICON.getIconWidth();
321     }
322
323     @Override
324     public int getIconHeight() {
325       return DEFAULT_ICON.getIconHeight();
326     }
327
328     @Override
329     public boolean equals(Object o) {
330       if (this == o) return true;
331       if (o == null || getClass() != o.getClass()) return false;
332
333       MnemonicIcon that = (MnemonicIcon)o;
334
335       return myMnemonic == that.myMnemonic;
336     }
337
338     @Override
339     public int hashCode() {
340       return (int)myMnemonic;
341     }
342   }
343
344   private static class MyCheckedIcon implements Icon {
345     @Override
346     public void paintIcon(Component c, Graphics g, int x, int y) {
347       (darkBackground() ? AllIcons.Actions.CheckedGrey : AllIcons.Actions.CheckedBlack).paintIcon(c, g, x, y);
348     }
349
350     @Override
351     public int getIconWidth() {
352       return PlatformIcons.CHECK_ICON.getIconWidth();
353     }
354
355     @Override
356     public int getIconHeight() {
357       return PlatformIcons.CHECK_ICON.getIconHeight();
358     }
359   }
360
361   private static boolean darkBackground() {
362     return ColorUtil.isDark(EditorColorsManager.getInstance().getGlobalScheme().getColor(EditorColors.GUTTER_BACKGROUND));
363   }
364
365   private static class MyGutterIconRenderer extends GutterIconRenderer {
366     private final Bookmark myBookmark;
367
368     public MyGutterIconRenderer(@NotNull Bookmark bookmark) {
369       myBookmark = bookmark;
370     }
371
372     @Override
373     @NotNull
374     public Icon getIcon() {
375       return myBookmark.getIcon();
376     }
377
378     @Override
379     public String getTooltipText() {
380       return myBookmark.getBookmarkTooltip();
381     }
382
383     @Override
384     public boolean equals(Object obj) {
385       return obj instanceof MyGutterIconRenderer &&
386              Comparing.equal(getTooltipText(), ((MyGutterIconRenderer)obj).getTooltipText()) &&
387              Comparing.equal(getIcon(), ((MyGutterIconRenderer)obj).getIcon());
388     }
389
390     @Override
391      public int hashCode() {
392       return getIcon().hashCode();
393     }
394   }
395 }