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