merging of editor tooltip changes
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / editor / impl / EditorMarkupModelImpl.java
1 /*
2  * Copyright 2000-2009 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 /*
18  * Created by IntelliJ IDEA.
19  * User: max
20  * Date: Apr 19, 2002
21  * Time: 2:56:43 PM
22  * To change template for new class use
23  * Code Style | Class Templates options (Tools | IDE Options).
24  */
25 package com.intellij.openapi.editor.impl;
26
27 import com.intellij.codeInsight.hint.LineTooltipRenderer;
28 import com.intellij.codeInsight.hint.TooltipController;
29 import com.intellij.codeInsight.hint.TooltipGroup;
30 import com.intellij.codeInsight.hint.TooltipRenderer;
31 import com.intellij.openapi.application.ApplicationManager;
32 import com.intellij.openapi.application.ex.ApplicationManagerEx;
33 import com.intellij.openapi.application.impl.ApplicationImpl;
34 import com.intellij.openapi.command.CommandProcessor;
35 import com.intellij.openapi.command.UndoConfirmationPolicy;
36 import com.intellij.openapi.diagnostic.Logger;
37 import com.intellij.openapi.editor.*;
38 import com.intellij.openapi.editor.actionSystem.DocCommandGroupId;
39 import com.intellij.openapi.editor.ex.*;
40 import com.intellij.openapi.editor.markup.ErrorStripeRenderer;
41 import com.intellij.openapi.editor.markup.RangeHighlighter;
42 import com.intellij.openapi.ui.popup.Balloon;
43 import com.intellij.openapi.util.IconLoader;
44 import com.intellij.ui.ColorUtil;
45 import com.intellij.ui.HintHint;
46 import com.intellij.ui.LightweightHint;
47 import com.intellij.ui.PopupHandler;
48 import com.intellij.util.Processor;
49 import com.intellij.util.ui.ButtonlessScrollBarUI;
50 import com.intellij.util.ui.UIUtil;
51 import gnu.trove.THashSet;
52 import org.jetbrains.annotations.NotNull;
53
54 import javax.swing.*;
55 import javax.swing.plaf.ScrollBarUI;
56 import java.awt.*;
57 import java.awt.event.MouseEvent;
58 import java.awt.event.MouseListener;
59 import java.awt.event.MouseMotionListener;
60 import java.util.*;
61 import java.util.List;
62 import java.util.Queue;
63
64 public class EditorMarkupModelImpl extends MarkupModelImpl implements EditorMarkupModel {
65   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorMarkupModelImpl");
66
67   private static final TooltipGroup ERROR_STRIPE_TOOLTIP_GROUP = new TooltipGroup("ERROR_STRIPE_TOOLTIP_GROUP", 0);
68   private static final Icon ERRORS_FOUND_ICON = IconLoader.getIcon("/general/errorsFound.png");
69   private static final int ERROR_ICON_WIDTH = ERRORS_FOUND_ICON.getIconWidth();
70   private static final int ERROR_ICON_HEIGHT = ERRORS_FOUND_ICON.getIconHeight();
71   private static final int PREFERRED_WIDTH = ERRORS_FOUND_ICON.getIconWidth() + 4;
72   private final EditorImpl myEditor;
73   private ErrorStripeRenderer myErrorStripeRenderer = null;
74   private final List<ErrorStripeListener> myErrorMarkerListeners = new ArrayList<ErrorStripeListener>();
75   private ErrorStripeListener[] myCachedErrorMarkerListeners = null;
76
77   private int myEditorScrollbarTop = -1;
78   private int myEditorTargetHeight = -1;
79   private int myEditorSourceHeight = -1;
80
81   @NotNull private ErrorStripTooltipRendererProvider myTooltipRendererProvider = new BasicTooltipRendererProvider();
82
83   private static int myMinMarkHeight = 3;
84
85   EditorMarkupModelImpl(@NotNull EditorImpl editor) {
86     super((DocumentImpl)editor.getDocument());
87     myEditor = editor;
88   }
89
90   @Override
91   protected void assertDispatchThread() {
92     ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(myEditor.getComponent());
93   }
94
95   private int offsetToLine(int offset) {
96     final Document document = myEditor.getDocument();
97     if (offset > document.getTextLength()) {
98       return document.getLineCount();
99     }
100     return myEditor.offsetToVisualLine(offset);
101   }
102
103   private static int getMinHeight() {
104     return myMinMarkHeight;
105   }
106
107   private void recalcEditorDimensions() {
108     EditorImpl.MyScrollBar scrollBar = myEditor.getVerticalScrollBar();
109     int scrollBarHeight = scrollBar.getSize().height;
110
111     myEditorScrollbarTop = scrollBar.getDecScrollButtonHeight()/* + 1*/;
112     int editorScrollbarBottom = scrollBar.getIncScrollButtonHeight();
113     myEditorTargetHeight = scrollBarHeight - myEditorScrollbarTop - editorScrollbarBottom;
114     myEditorSourceHeight = myEditor.getPreferredHeight();
115   }
116
117   public void repaintTrafficLightIcon() {
118     MyErrorPanel errorPanel = getErrorPanel();
119     if (errorPanel != null) {
120       errorPanel.myErrorStripeButton.repaint();
121       errorPanel.repaintTrafficTooltip();
122     }
123   }
124
125   private static class PositionedStripe {
126     private final Color color;
127     private final int yStart;
128     private int yEnd;
129     private final boolean thin;
130     private final int layer;
131
132     private PositionedStripe(Color color, int yStart, int yEnd, boolean thin, int layer) {
133       this.color = color;
134       this.yStart = yStart;
135       this.yEnd = yEnd;
136       this.thin = thin;
137       this.layer = layer;
138     }
139   }
140
141   private boolean showToolTipByMouseMove(final MouseEvent e, final double width) {
142     Set<RangeHighlighter> highlighters = new THashSet<RangeHighlighter>();
143
144     getNearestHighlighters(this, e, width, highlighters);
145     getNearestHighlighters((MarkupModelEx)myEditor.getDocument().getMarkupModel(getEditor().getProject()), e, width, highlighters);
146     if (highlighters.isEmpty()) return false;
147     TooltipRenderer bigRenderer = myTooltipRendererProvider.calcTooltipRenderer(highlighters);
148     if (bigRenderer != null) {
149       showTooltip(e, bigRenderer, new HintHint(e).setAwtTooltip(true).setPreferredPosition(Balloon.Position.atLeft));
150       return true;
151     }
152     return false;
153   }
154
155   private RangeHighlighter getNearestRangeHighlighter(final MouseEvent e, final int width) {
156     List<RangeHighlighter> highlighters = new ArrayList<RangeHighlighter>();
157     getNearestHighlighters(this, e, width, highlighters);
158     getNearestHighlighters((MarkupModelEx)myEditor.getDocument().getMarkupModel(myEditor.getProject()), e, width, highlighters);
159     RangeHighlighter nearestMarker = null;
160     int yPos = 0;
161     for (RangeHighlighter highlighter : highlighters) {
162       final int newYPos = offsetToYPosition(highlighter.getStartOffset());
163
164       if (nearestMarker == null || Math.abs(yPos - e.getY()) > Math.abs(newYPos - e.getY())) {
165         nearestMarker = highlighter;
166         yPos = newYPos;
167       }
168     }
169     return nearestMarker;
170   }
171
172   private void getNearestHighlighters(MarkupModelEx markupModel, MouseEvent e, final double width, final Collection<RangeHighlighter> nearest) {
173     if (0 > e.getX() || e.getX() >= width) return;
174     int startOffset = yPositionToOffset(e.getY()-getMinHeight(), true);
175     int endOffset = yPositionToOffset(e.getY()+getMinHeight(), false);
176     markupModel.processHighlightsOverlappingWith(startOffset, endOffset, new Processor<RangeHighlighterEx>() {
177       public boolean process(RangeHighlighterEx highlighter) {
178         if (highlighter.getErrorStripeMarkColor() != null) nearest.add(highlighter);
179         return true;
180       }
181     });
182   }
183
184   public void doClick(final MouseEvent e, final int width) {
185     RangeHighlighter marker = getNearestRangeHighlighter(e, width);
186     if (marker == null) return;
187     int offset = marker.getStartOffset();
188
189     final Document doc = myEditor.getDocument();
190     if (doc.getLineCount() > 0) {
191       // Necessary to expand folded block even if navigating just before one
192       // Very useful when navigating to first unused import statement.
193       int lineEnd = doc.getLineEndOffset(doc.getLineNumber(offset));
194       myEditor.getCaretModel().moveToOffset(lineEnd);
195     }
196
197     myEditor.getCaretModel().moveToOffset(offset);
198     myEditor.getSelectionModel().removeSelection();
199     ScrollingModel scrollingModel = myEditor.getScrollingModel();
200     scrollingModel.disableAnimation();
201     scrollingModel.scrollToCaret(ScrollType.CENTER);
202     scrollingModel.enableAnimation();
203     fireErrorMarkerClicked(marker, e);
204   }
205
206   public void setErrorStripeVisible(boolean val) {
207     if (val) {
208       myEditor.getVerticalScrollBar().setPersistentUI(new MyErrorPanel());
209     }
210     else {
211       myEditor.getVerticalScrollBar().setPersistentUI(ButtonlessScrollBarUI.createNormal());
212     }
213   }
214   private MyErrorPanel getErrorPanel() {
215     ScrollBarUI ui = myEditor.getVerticalScrollBar().getUI();
216     return ui instanceof MyErrorPanel ? (MyErrorPanel)ui : null;
217   }
218
219   public void setErrorPanelPopupHandler(@NotNull PopupHandler handler) {
220     MyErrorPanel errorPanel = getErrorPanel();
221     if (errorPanel != null) {
222       errorPanel.setPopupHandler(handler);
223     }
224   }
225
226   public void setErrorStripTooltipRendererProvider(@NotNull final ErrorStripTooltipRendererProvider provider) {
227     myTooltipRendererProvider = provider;
228   }
229
230   @NotNull
231   public ErrorStripTooltipRendererProvider getErrorStripTooltipRendererProvider() {
232     return myTooltipRendererProvider;
233   }
234
235   @NotNull
236   public Editor getEditor() {
237     return myEditor;
238   }
239
240   public void setErrorStripeRenderer(ErrorStripeRenderer renderer) {
241     assertIsDispatchThread();
242     myErrorStripeRenderer = renderer;
243     //try to not cancel tooltips here, since it is being called after every writeAction, even to the console
244     //HintManager.getInstance().getTooltipController().cancelTooltips();
245
246     myEditor.getVerticalScrollBar().repaint();
247   }
248
249   private void assertIsDispatchThread() {
250     ApplicationManagerEx.getApplicationEx().assertIsDispatchThread(myEditor.getComponent());
251   }
252
253   public ErrorStripeRenderer getErrorStripeRenderer() {
254     return myErrorStripeRenderer;
255   }
256
257   public void dispose() {
258     myErrorStripeRenderer = null;
259     super.dispose();
260   }
261
262   void repaint(int startOffset, int endOffset) {
263     markDirtied();
264
265     int startY = offsetToYPosition(startOffset);
266     int endY = offsetToYPosition(endOffset) + getMinHeight();
267
268     myEditor.getVerticalScrollBar().repaint(0, startY, PREFERRED_WIDTH, endY - startY);
269   }
270
271   private static final Dimension STRIPE_BUTTON_PREFERRED_SIZE = new Dimension(PREFERRED_WIDTH, ERROR_ICON_HEIGHT + 4);
272   private class ErrorStripeButton extends JButton {
273     private ErrorStripeButton() {
274     }
275
276     @Override
277     public void paint(Graphics g) {
278       ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintStart();
279
280       final Rectangle bounds = getBounds();
281
282       g.setColor(myEditor.getBackgroundColor());
283       g.fillRect(0, 0, bounds.width, bounds.height);
284
285       g.setColor(ButtonlessScrollBarUI.TRACK_BACKGROUND);
286       g.fillRect(3, 0, bounds.width, bounds.height);
287
288       g.setColor(ButtonlessScrollBarUI.TRACK_BORDER);
289       g.drawLine(3, 0, 3, bounds.height);
290
291       try {
292         if (myErrorStripeRenderer != null) {
293           myErrorStripeRenderer.paint(this, g, new Rectangle(5, 2, ERROR_ICON_WIDTH, ERROR_ICON_HEIGHT));
294         }
295       }
296       finally {
297         ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintFinish();
298       }
299     }
300
301     @Override
302     public Dimension getPreferredSize() {
303       return STRIPE_BUTTON_PREFERRED_SIZE;
304     }
305   }
306
307   private class MyErrorPanel extends ButtonlessScrollBarUI implements MouseMotionListener, MouseListener {
308     private PopupHandler myHandler;
309     private ErrorStripeButton myErrorStripeButton;
310
311     @Override
312     protected JButton createDecreaseButton(int orientation) {
313       myErrorStripeButton = new ErrorStripeButton();
314       return myErrorStripeButton;
315     }
316
317     @Override
318     protected void installListeners() {
319       super.installListeners();
320       scrollbar.addMouseMotionListener(this);
321       scrollbar.addMouseListener(this);
322       myErrorStripeButton.addMouseMotionListener(this);
323       myErrorStripeButton.addMouseListener(this);
324     }
325
326     @Override
327     protected void uninstallListeners() {
328       scrollbar.removeMouseMotionListener(this);
329       scrollbar.removeMouseListener(this);
330       myErrorStripeButton.removeMouseMotionListener(this);
331       myErrorStripeButton.removeMouseListener(this);
332       super.uninstallListeners();
333     }
334
335     @Override
336     protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
337       g.translate(-2, 0);
338       g.setColor(Color.white);
339       final Rectangle oldClip = g.getClipBounds();
340       g.setClip(thumbBounds.x, thumbBounds.y, 5, thumbBounds.height );
341       g.fillRect(thumbBounds.x + 2, thumbBounds.y + 2, thumbBounds.width, thumbBounds.height - 4);
342
343       g.setClip(oldClip);
344       super.paintThumb(g, c, thumbBounds);
345       g.translate(2, 0);
346     }
347
348     @Override
349     protected int adjustThumbWidth(int width) {
350       return width - 5;
351     }
352
353     @Override
354     protected int getThickness() {
355       return super.getThickness() + 5;
356     }
357
358     @Override
359     protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {
360       Rectangle bounds = new Rectangle(trackBounds);
361
362       g.setColor(myEditor.getBackgroundColor());
363       g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
364
365       bounds.width /= 1.20;
366       final int shift = trackBounds.width - bounds.width;
367
368       g.translate(shift, 0);
369
370       super.paintTrack(g, c, bounds);
371
372       ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintStart();
373
374       try {
375         Rectangle clipBounds = g.getClipBounds();
376         repaint(g, ERROR_ICON_WIDTH - 2, clipBounds);
377       }
378       finally {
379         g.translate(-shift, 0);
380         ((ApplicationImpl)ApplicationManager.getApplication()).editorPaintFinish();
381       }
382     }
383
384     @Override
385     protected Color adjustColor(Color c) {
386       return ColorUtil.withAlpha(super.adjustColor(c), 0.85);
387     }
388
389     private void repaint(final Graphics g, final int width, Rectangle clipBounds) {
390       Document document = myEditor.getDocument();
391       int startOffset = yPositionToOffset(clipBounds.y, true);
392       int endOffset = yPositionToOffset(clipBounds.y + clipBounds.height, false);
393
394       drawMarkup(g, width, startOffset, endOffset, EditorMarkupModelImpl.this);
395       drawMarkup(g, width, startOffset, endOffset, (MarkupModelEx)document.getMarkupModel(myEditor.getProject()));
396     }
397
398     private void drawMarkup(final Graphics g, final int width, int startOffset, int endOffset, MarkupModelEx markup) {
399       final Queue<PositionedStripe> thinEnds = new PriorityQueue<PositionedStripe>(5, new Comparator<PositionedStripe>() {
400         public int compare(PositionedStripe o1, PositionedStripe o2) {
401           return o1.yEnd - o2.yEnd;
402         }
403       });
404       final Queue<PositionedStripe> wideEnds = new PriorityQueue<PositionedStripe>(5, new Comparator<PositionedStripe>() {
405         public int compare(PositionedStripe o1, PositionedStripe o2) {
406           return o1.yEnd - o2.yEnd;
407         }
408       });
409       // sorted by layer
410       final List<PositionedStripe> thinStripes = new ArrayList<PositionedStripe>();
411       final List<PositionedStripe> wideStripes = new ArrayList<PositionedStripe>();
412       final int[] thinYStart = new int[1];  // in range 0..yStart all spots are drawn
413       final int[] wideYStart = new int[1];  // in range 0..yStart all spots are drawn
414
415       markup.processHighlightsOverlappingWith(startOffset, endOffset, new Processor<RangeHighlighterEx>() {
416         public boolean process(RangeHighlighterEx highlighter) {
417           Color color = highlighter.getErrorStripeMarkColor();
418           if (color == null) return true;
419           boolean isThin = highlighter.isThinErrorStripeMark();
420           int[] yStart = isThin ? thinYStart : wideYStart;
421           List<PositionedStripe> stripes = isThin ? thinStripes : wideStripes;
422           Queue<PositionedStripe> ends = isThin ? thinEnds : wideEnds;
423
424           final int ys = offsetToYPosition(highlighter.getStartOffset());
425           int ye = offsetToYPosition(highlighter.getEndOffset());
426           if (ye - ys < getMinHeight()) ye = ys + getMinHeight();
427
428           yStart[0] = drawStripesEndingBefore(ys, ends, stripes, g, width, yStart[0]);
429
430           final int layer = highlighter.getLayer();
431
432           PositionedStripe stripe = null;
433           int i;
434           for (i = 0; i < stripes.size(); i++) {
435             PositionedStripe s = stripes.get(i);
436             if (s.layer == layer) {
437               stripe = s;
438               break;
439             }
440             if (s.layer < layer) {
441               break;
442             }
443           }
444           if (stripe == null) {
445             // started new stripe, draw previous above
446             if (yStart[0] != ys) {
447               if (!stripes.isEmpty()) {
448                 PositionedStripe top = stripes.get(0);
449                 drawSpot(g, width, top.thin, yStart[0], ys, top.color, true, true);
450               }
451               yStart[0] = ys;
452             }
453             stripe = new PositionedStripe(color, ys, ye, isThin, layer);
454             stripes.add(i, stripe);
455             ends.offer(stripe);
456           }
457           else {
458             // key changed, reinsert into queue
459             if (stripe.yEnd != ye) {
460               ends.remove(stripe);
461               stripe.yEnd = ye;
462               ends.offer(stripe);
463             }
464           }
465
466           return true;
467         }
468
469       });
470
471       drawStripesEndingBefore(Integer.MAX_VALUE, thinEnds, thinStripes, g, width, thinYStart[0]);
472       drawStripesEndingBefore(Integer.MAX_VALUE, wideEnds, wideStripes, g, width, wideYStart[0]);
473     }
474
475     private int drawStripesEndingBefore(int ys,
476                                         Queue<PositionedStripe> ends,
477                                         List<PositionedStripe> stripes,
478                                         Graphics g, int width, int yStart) {
479       while (!ends.isEmpty()) {
480         PositionedStripe endingStripe = ends.peek();
481         if (endingStripe.yEnd > ys) break;
482         ends.remove();
483
484         // check whether endingStripe got obscured in the range yStart..endingStripe.yEnd
485         int i = stripes.indexOf(endingStripe);
486         stripes.remove(i);
487         if (i == 0) {
488           // visible
489           drawSpot(g, width, endingStripe.thin, yStart, endingStripe.yEnd, endingStripe.color, true, true);
490           yStart = endingStripe.yEnd;
491         }
492       }
493       return yStart;
494     }
495
496     private void drawSpot(Graphics g,
497                           int width,
498                           boolean thinErrorStripeMark,
499                           int yStart,
500                           int yEnd,
501                           Color color,
502                           boolean drawTopDecoration,
503                           boolean drawBottomDecoration) {
504       int x = 3;
505       int paintWidth = width;
506       if (thinErrorStripeMark) {
507         paintWidth /= 2;
508         paintWidth += 1;
509         x += paintWidth + 1;
510       }
511       if (color == null) return;
512       g.setColor(color);
513       g.fillRect(x + 1, yStart, paintWidth - 2, yEnd - yStart+1);
514
515       Color brighter = color.brighter();
516       g.setColor(brighter);
517       //left decoration
518       UIUtil.drawLine(g, x, yStart, x, yEnd/* - 1*/);
519       if (drawTopDecoration) {
520         //top decoration
521         UIUtil.drawLine(g, x + 1, yStart, x + paintWidth - 2, yStart);
522       }
523       Color darker = ColorUtil.shift(color, 0.75);
524
525       g.setColor(darker);
526       if (drawBottomDecoration) {
527         // bottom decoration
528         UIUtil.drawLine(g, x + 1, yEnd/* - 1*/, x + paintWidth - 2, yEnd/* - 1*/);   // large bottom to let overwrite by hl below
529       }
530       //right decoration
531       UIUtil.drawLine(g, x + paintWidth - 2, yStart, x + paintWidth - 2, yEnd/* - 1*/);
532     }
533
534     // mouse events
535     public void mouseClicked(final MouseEvent e) {
536       CommandProcessor.getInstance().executeCommand(myEditor.getProject(), new Runnable() {
537           public void run() {
538             doMouseClicked(e);
539           }
540         },
541         EditorBundle.message("move.caret.command.name"), DocCommandGroupId.noneGroupId(getDocument()), UndoConfirmationPolicy.DEFAULT, getDocument()
542       );
543     }
544
545     public void mousePressed(MouseEvent e) {
546     }
547
548     public void mouseReleased(MouseEvent e) {
549     }
550
551     private int getWidth() {
552       return scrollbar.getWidth();
553     }
554
555     private void doMouseClicked(MouseEvent e) {
556       myEditor.getContentComponent().requestFocus();
557       int lineCount = getDocument().getLineCount() + myEditor.getSettings().getAdditionalLinesCount();
558       if (lineCount == 0) {
559         return;
560       }
561       doClick(e, getWidth());
562     }
563
564     public void mouseMoved(MouseEvent e) {
565       EditorImpl.MyScrollBar scrollBar = myEditor.getVerticalScrollBar();
566       int buttonHeight = scrollBar.getDecScrollButtonHeight();
567       int lineCount = getDocument().getLineCount() + myEditor.getSettings().getAdditionalLinesCount();
568       if (lineCount == 0) {
569         return;
570       }
571
572       if (e.getY() < buttonHeight && myErrorStripeRenderer != null) {
573         showTrafficLightTooltip(e);
574         return;
575       }
576
577       if (showToolTipByMouseMove(e,getWidth())) {
578         scrollbar.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
579         return;
580       }
581
582       cancelMyToolTips(e);
583
584       if (scrollbar.getCursor().equals(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))) {
585         scrollbar.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
586       }
587     }
588
589     private TrafficTooltipRenderer myTrafficTooltipRenderer;
590     private void showTrafficLightTooltip(MouseEvent e) {
591       //final String tooltipMessage = myErrorStripeRenderer.getTooltipMessage();
592       //if (tooltipMessage == null) return;
593       if (myTrafficTooltipRenderer == null) {
594         myTrafficTooltipRenderer = myTooltipRendererProvider.createTrafficTooltipRenderer(new Runnable() {
595           @Override
596           public void run() {
597             myTrafficTooltipRenderer = null;
598           }
599         });
600       }
601       showTooltip(e, myTrafficTooltipRenderer, new HintHint(e).setAwtTooltip(true).setPreferredPosition(Balloon.Position.atLeft));
602     }
603
604     private void repaintTrafficTooltip() {
605       if (myTrafficTooltipRenderer != null) {
606         myTrafficTooltipRenderer.repaintTooltipWindow();
607       }
608     }
609
610     private void cancelMyToolTips(final MouseEvent e) {
611       final TooltipController tooltipController = TooltipController.getInstance();
612       if (!tooltipController.shouldSurvive(e)) {
613         tooltipController.cancelTooltip(ERROR_STRIPE_TOOLTIP_GROUP);
614       }
615     }
616
617     public void mouseEntered(MouseEvent e) {
618     }
619
620     public void mouseExited(MouseEvent e) {
621       cancelMyToolTips(e);
622     }
623
624     public void mouseDragged(MouseEvent e) {
625       cancelMyToolTips(e);
626     }
627
628     public void setPopupHandler(final PopupHandler handler) {
629       if (myHandler != null) {
630         scrollbar.removeMouseListener(myHandler);
631         myErrorStripeButton.removeMouseListener(myHandler);
632       }
633
634       myHandler = handler;
635       scrollbar.addMouseListener(handler);
636       myErrorStripeButton.addMouseListener(myHandler);
637     }
638   }
639
640   private void showTooltip(MouseEvent e, final TooltipRenderer tooltipObject, HintHint hintHint) {
641     TooltipController tooltipController = TooltipController.getInstance();
642     tooltipController.showTooltipByMouseMove(myEditor, e, tooltipObject,
643                                              myEditor.getVerticalScrollbarOrientation() == EditorEx.VERTICAL_SCROLLBAR_RIGHT,
644                                              ERROR_STRIPE_TOOLTIP_GROUP, hintHint);
645   }
646
647   private ErrorStripeListener[] getCachedErrorMarkerListeners() {
648     if (myCachedErrorMarkerListeners == null) {
649       myCachedErrorMarkerListeners = myErrorMarkerListeners.toArray(new ErrorStripeListener[myErrorMarkerListeners.size()]);
650     }
651
652     return myCachedErrorMarkerListeners;
653   }
654
655   private void fireErrorMarkerClicked(RangeHighlighter marker, MouseEvent e) {
656     ErrorStripeEvent event = new ErrorStripeEvent(getEditor(), e, marker);
657     ErrorStripeListener[] listeners = getCachedErrorMarkerListeners();
658     for (ErrorStripeListener listener : listeners) {
659       listener.errorMarkerClicked(event);
660     }
661   }
662
663   public void addErrorMarkerListener(@NotNull ErrorStripeListener listener) {
664     myCachedErrorMarkerListeners = null;
665     myErrorMarkerListeners.add(listener);
666   }
667
668   public void removeErrorMarkerListener(@NotNull ErrorStripeListener listener) {
669     myCachedErrorMarkerListeners = null;
670     boolean success = myErrorMarkerListeners.remove(listener);
671     LOG.assertTrue(success);
672   }
673
674   public void markDirtied() {
675     myEditorScrollbarTop = -1;
676     myEditorSourceHeight = -1;
677     myEditorTargetHeight = -1;
678   }
679
680   public void setMinMarkHeight(final int minMarkHeight) {
681     myMinMarkHeight = minMarkHeight;
682   }
683
684   private static class BasicTooltipRendererProvider implements ErrorStripTooltipRendererProvider {
685     public TooltipRenderer calcTooltipRenderer(@NotNull final Collection<RangeHighlighter> highlighters) {
686       LineTooltipRenderer bigRenderer = null;
687       //do not show same tooltip twice
688       Set<String> tooltips = null;
689
690       for (RangeHighlighter highlighter : highlighters) {
691         final Object tooltipObject = highlighter.getErrorStripeTooltip();
692         if (tooltipObject == null) continue;
693
694         final String text = tooltipObject.toString();
695         if (tooltips == null) {
696           tooltips = new THashSet<String>();
697         }
698         if (tooltips.add(text)) {
699           if (bigRenderer == null) {
700             bigRenderer = new LineTooltipRenderer(text);
701           }
702           else {
703             bigRenderer.addBelow(text);
704           }
705         }
706       }
707
708       return bigRenderer;
709     }
710
711     public TooltipRenderer calcTooltipRenderer(@NotNull final String text) {
712       return new LineTooltipRenderer(text);
713     }
714
715     public TooltipRenderer calcTooltipRenderer(@NotNull final String text, final int width) {
716       return new LineTooltipRenderer(text, width);
717     }
718
719     @Override
720     public TrafficTooltipRenderer createTrafficTooltipRenderer(final Runnable onHide) {
721       return new TrafficTooltipRenderer() {
722         @Override
723         public void repaintTooltipWindow() {
724         }
725
726         @Override
727         public LightweightHint show(Editor editor, Point p, boolean alignToRight, TooltipGroup group, HintHint hintHint) {
728           JLabel label = new JLabel("WTF");
729           return new LightweightHint(label){
730             @Override
731             public void hide() {
732               super.hide();
733               onHide.run();
734             }
735           };
736         }
737       };
738     }
739   }
740
741   private int offsetToYPosition(int offset) {
742     int lineNumber = offsetToLine(offset);
743     if (myEditorScrollbarTop == -1 || myEditorTargetHeight == -1) {
744       recalcEditorDimensions();
745     }
746     if (myEditorSourceHeight < myEditorTargetHeight) {
747       return myEditorScrollbarTop + lineNumber * myEditor.getLineHeight();
748     }
749     else {
750       final int lineCount = myEditorSourceHeight / myEditor.getLineHeight();
751       return myEditorScrollbarTop + (int)((float)lineNumber / lineCount * myEditorTargetHeight);
752     }
753   }
754
755   private int yPositionToOffset(int y, boolean beginLine) {
756     if (myEditorScrollbarTop == -1 || myEditorTargetHeight == -1) {
757       recalcEditorDimensions();
758     }
759     final int safeY = Math.max(0, y - myEditorScrollbarTop);
760     VisualPosition visual;
761     if (myEditorSourceHeight < myEditorTargetHeight) {
762       visual = myEditor.xyToVisualPosition(new Point(0, safeY));
763     }
764     else {
765       float fraction = Math.max(0, Math.min(1, safeY / (float)myEditorTargetHeight));
766       final int lineCount = myEditorSourceHeight / myEditor.getLineHeight();
767       visual = new VisualPosition((int)(fraction * lineCount),0);
768     }
769     int line = myEditor.visualToLogicalPosition(visual).line;
770     Document document = myEditor.getDocument();
771     if (line < 0) return 0;
772     if (line >= document.getLineCount()) return document.getTextLength();
773
774     return beginLine ? document.getLineStartOffset(line) : document.getLineEndOffset(line);
775   }
776 }