6c7a28854914a1e64829ce7fc49eb6011c242325
[idea/community.git] / platform / platform-impl / src / com / intellij / codeInsight / hint / HintManagerImpl.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 package com.intellij.codeInsight.hint;
17
18 import com.intellij.ide.IdeTooltip;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.actionSystem.*;
21 import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
22 import com.intellij.openapi.actionSystem.ex.AnActionListener;
23 import com.intellij.openapi.application.ApplicationManager;
24 import com.intellij.openapi.components.ServiceManager;
25 import com.intellij.openapi.diagnostic.Logger;
26 import com.intellij.openapi.editor.Editor;
27 import com.intellij.openapi.editor.LogicalPosition;
28 import com.intellij.openapi.editor.event.*;
29 import com.intellij.openapi.editor.event.DocumentAdapter;
30 import com.intellij.openapi.editor.ex.EditorEx;
31 import com.intellij.openapi.editor.markup.*;
32 import com.intellij.openapi.fileEditor.FileEditorManagerAdapter;
33 import com.intellij.openapi.fileEditor.FileEditorManagerEvent;
34 import com.intellij.openapi.fileEditor.FileEditorManagerListener;
35 import com.intellij.openapi.project.Project;
36 import com.intellij.openapi.project.ProjectManager;
37 import com.intellij.openapi.project.ProjectManagerAdapter;
38 import com.intellij.openapi.ui.popup.Balloon;
39 import com.intellij.openapi.ui.popup.JBPopup;
40 import com.intellij.openapi.ui.popup.JBPopupFactory;
41 import com.intellij.openapi.util.registry.Registry;
42 import com.intellij.ui.*;
43 import com.intellij.ui.awt.RelativePoint;
44 import com.intellij.util.Alarm;
45 import com.intellij.util.ui.UIUtil;
46 import org.jetbrains.annotations.NotNull;
47
48 import javax.swing.*;
49 import java.awt.*;
50 import java.awt.event.*;
51 import java.util.ArrayList;
52 import java.util.EventObject;
53 import java.util.List;
54
55 public class HintManagerImpl extends HintManager implements Disposable {
56   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.hint.HintManager");
57
58   private final AnActionListener myAnActionListener;
59   private final MyEditorManagerListener myEditorManagerListener;
60   private final EditorMouseAdapter myEditorMouseListener;
61   private final FocusListener myEditorFocusListener;
62   private final DocumentListener myEditorDocumentListener;
63   private final VisibleAreaListener myVisibleAreaListener;
64   private final CaretListener myCaretMoveListener;
65
66   private LightweightHint myQuestionHint = null;
67   private QuestionAction myQuestionAction = null;
68
69   private final List<HintInfo> myHintsStack = new ArrayList<HintInfo>();
70   private Editor myLastEditor = null;
71   private final Alarm myHideAlarm = new Alarm();
72
73   private static int getPriority(QuestionAction action) {
74     return action instanceof PriorityQuestionAction ? ((PriorityQuestionAction)action).getPriority() : 0;
75   }
76
77   public boolean canShowQuestionAction(QuestionAction action) {
78     ApplicationManager.getApplication().assertIsDispatchThread();
79     return myQuestionAction == null || getPriority(myQuestionAction) <= getPriority(action);
80   }
81
82   public interface ActionToIgnore {
83   }
84
85   private static class HintInfo {
86     final LightweightHint hint;
87     @HideFlags final int flags;
88     private final boolean reviveOnEditorChange;
89
90     private HintInfo(LightweightHint hint, @HideFlags int flags, boolean reviveOnEditorChange) {
91       this.hint = hint;
92       this.flags = flags;
93       this.reviveOnEditorChange = reviveOnEditorChange;
94     }
95   }
96
97   public static HintManagerImpl getInstanceImpl() {
98     return (HintManagerImpl)ServiceManager.getService(HintManager.class);
99   }
100
101   public HintManagerImpl(ActionManagerEx actionManagerEx, ProjectManager projectManager) {
102     myEditorManagerListener = new MyEditorManagerListener();
103
104     myAnActionListener = new MyAnActionListener();
105     actionManagerEx.addAnActionListener(myAnActionListener);
106
107     myCaretMoveListener = new CaretAdapter() {
108       @Override
109       public void caretPositionChanged(CaretEvent e) {
110         hideHints(HIDE_BY_ANY_KEY, false, false);
111       }
112     };
113
114     projectManager.addProjectManagerListener(new MyProjectManagerListener());
115
116     myEditorMouseListener = new EditorMouseAdapter() {
117       @Override
118       public void mousePressed(EditorMouseEvent event) {
119         hideAllHints();
120       }
121     };
122
123     myVisibleAreaListener = new VisibleAreaListener() {
124       @Override
125       public void visibleAreaChanged(VisibleAreaEvent e) {
126         updateScrollableHints(e);
127         hideHints(HIDE_BY_SCROLLING, false, false);
128       }
129     };
130
131     myEditorFocusListener = new FocusAdapter() {
132       @Override
133       public void focusLost(final FocusEvent e) {
134         //if (UIUtil.isFocusProxy(e.getOppositeComponent())) return;
135         myHideAlarm.addRequest(new Runnable() {
136           @Override
137           public void run() {
138             if (!JBPopupFactory.getInstance().isChildPopupFocused(e.getComponent())) {
139               hideAllHints();
140             }
141           }
142         }, 200);
143       }
144
145       @Override
146       public void focusGained(FocusEvent e) {
147         myHideAlarm.cancelAllRequests();
148       }
149     };
150
151     myEditorDocumentListener = new DocumentAdapter() {
152       @Override
153       public void documentChanged(DocumentEvent event) {
154         LOG.assertTrue(SwingUtilities.isEventDispatchThread());
155         HintInfo[] infos = getHintsStackArray();
156         for (HintInfo info : infos) {
157           if ((info.flags & HIDE_BY_TEXT_CHANGE) != 0) {
158             if (info.hint.isVisible()) {
159               info.hint.hide();
160             }
161             myHintsStack.remove(info);
162           }
163         }
164
165         if (myHintsStack.isEmpty()) {
166           updateLastEditor(null);
167         }
168       }
169     };
170   }
171
172   @NotNull
173   private HintInfo[] getHintsStackArray() {
174     return myHintsStack.toArray(new HintInfo[myHintsStack.size()]);
175   }
176
177   public boolean performCurrentQuestionAction() {
178     if (myQuestionAction != null && myQuestionHint != null) {
179       if (myQuestionHint.isVisible()) {
180         if (LOG.isDebugEnabled()) {
181           LOG.debug("performing an action:" + myQuestionAction);
182         }
183         if (myQuestionAction.execute()) {
184           if (myQuestionHint != null) {
185             myQuestionHint.hide();
186           }
187         }
188         return true;
189       }
190
191       myQuestionAction = null;
192       myQuestionHint = null;
193     }
194
195     return false;
196   }
197
198
199   private void updateScrollableHints(VisibleAreaEvent e) {
200     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
201     for (HintInfo info : getHintsStackArray()) {
202       if (info.hint != null && (info.flags & UPDATE_BY_SCROLLING) != 0) {
203         updateScrollableHintPosition(e, info.hint, (info.flags & HIDE_IF_OUT_OF_EDITOR) != 0);
204       }
205     }
206   }
207
208   @Override
209   public boolean hasShownHintsThatWillHideByOtherHint(boolean willShowTooltip) {
210     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
211     for (HintInfo hintInfo : getHintsStackArray()) {
212       if (hintInfo.hint.isVisible() && (hintInfo.flags & HIDE_BY_OTHER_HINT) != 0) return true;
213       if (willShowTooltip && hintInfo.hint.isAwtTooltip()) {
214         // only one AWT tooltip can be visible, so this hint will hide even though it's not marked with HIDE_BY_OTHER_HINT
215         return true;
216       }
217     }
218     return false;
219   }
220
221   @Override
222   public void dispose() {
223     ActionManagerEx.getInstanceEx().removeAnActionListener(myAnActionListener);
224   }
225
226   private static void updateScrollableHintPosition(VisibleAreaEvent e, LightweightHint hint, boolean hideIfOutOfEditor) {
227     if (hint.getComponent() instanceof ScrollAwareHint) {
228       ((ScrollAwareHint)hint.getComponent()).editorScrolled();
229     }
230
231     if (!hint.isVisible()) return;
232
233     Editor editor = e.getEditor();
234     if (!editor.getComponent().isShowing() || editor.isOneLineMode()) return;
235     Rectangle newRectangle = e.getOldRectangle();
236     Rectangle oldRectangle = e.getNewRectangle();
237
238     Point location = hint.getLocationOn(editor.getContentComponent());
239     Dimension size = hint.getSize();
240
241     int xOffset = location.x - oldRectangle.x;
242     int yOffset = location.y - oldRectangle.y;
243     location = new Point(newRectangle.x + xOffset, newRectangle.y + yOffset);
244
245     Rectangle newBounds = new Rectangle(location.x, location.y, size.width, size.height);
246
247     final boolean okToUpdateBounds = hideIfOutOfEditor ? oldRectangle.contains(newBounds) : oldRectangle.intersects(newBounds);
248     if (okToUpdateBounds || hint.vetoesHiding()) {
249       hint.setLocation(new RelativePoint(editor.getContentComponent(), location));
250     }
251     else {
252       hint.hide();
253     }
254   }
255
256   public void showEditorHint(LightweightHint hint, Editor editor, @PositionFlags short constraint, @HideFlags int flags, int timeout, boolean reviveOnEditorChange) {
257     LogicalPosition pos = editor.getCaretModel().getLogicalPosition();
258     Point p = getHintPosition(hint, editor, pos, constraint);
259     showEditorHint(hint, editor, p, flags, timeout, reviveOnEditorChange, createHintHint(editor, p, hint, constraint));
260   }
261
262   /**
263    * @param p                    point in layered pane coordinate system.
264    * @param reviveOnEditorChange
265    */
266   public void showEditorHint(@NotNull final LightweightHint hint,
267                              @NotNull Editor editor,
268                              @NotNull Point p,
269                              @HideFlags int flags,
270                              int timeout,
271                              boolean reviveOnEditorChange) {
272
273     showEditorHint(hint, editor, p, flags, timeout, reviveOnEditorChange, HintManager.ABOVE);
274   }
275
276   public void showEditorHint(@NotNull final LightweightHint hint,
277                              @NotNull Editor editor,
278                              @NotNull Point p,
279                              @HideFlags int flags,
280                              int timeout,
281                              boolean reviveOnEditorChange,
282                              @PositionFlags short position) {
283
284     showEditorHint(hint, editor, p, flags, timeout, reviveOnEditorChange, createHintHint(editor, p, hint, position));
285   }
286
287   public void showEditorHint(@NotNull final LightweightHint hint,
288                              @NotNull Editor editor,
289                              @NotNull Point p,
290                              @HideFlags int flags,
291                              int timeout,
292                              boolean reviveOnEditorChange,
293                              HintHint hintInfo) {
294     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
295     myHideAlarm.cancelAllRequests();
296
297     hideHints(HIDE_BY_OTHER_HINT, false, false);
298
299     if (editor != myLastEditor) {
300       hideAllHints();
301     }
302
303     if (!ApplicationManager.getApplication().isUnitTestMode() && !editor.getContentComponent().isShowing()) return;
304     if (!ApplicationManager.getApplication().isActive()) return;
305
306     updateLastEditor(editor);
307
308     getPublisher().hintShown(editor.getProject(), hint, flags);
309
310     Component component = hint.getComponent();
311
312     doShowInGivenLocation(hint, editor, p, hintInfo, true);
313
314     ListenerUtil.addMouseListener(component, new MouseAdapter() {
315       @Override
316       public void mousePressed(MouseEvent e) {
317         myHideAlarm.cancelAllRequests();
318       }
319     });
320     ListenerUtil.addFocusListener(component, new FocusAdapter() {
321       @Override
322       public void focusGained(FocusEvent e) {
323         myHideAlarm.cancelAllRequests();
324       }
325     });
326
327     if ((flags & HIDE_BY_MOUSEOVER) != 0) {
328       ListenerUtil.addMouseMotionListener(component, new MouseMotionAdapter() {
329         @Override
330         public void mouseMoved(MouseEvent e) {
331           hideHints(HIDE_BY_MOUSEOVER, true, false);
332         }
333       });
334     }
335
336     myHintsStack.add(new HintInfo(hint, flags, reviveOnEditorChange));
337     if (timeout > 0) {
338       Timer timer = UIUtil.createNamedTimer("Hint timeout", timeout, new ActionListener() {
339         @Override
340         public void actionPerformed(ActionEvent event) {
341           hint.hide();
342         }
343       });
344       timer.setRepeats(false);
345       timer.start();
346     }
347   }
348
349   @Override
350   public void showHint(@NotNull final JComponent component, @NotNull RelativePoint p, int flags, int timeout) {
351     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
352     myHideAlarm.cancelAllRequests();
353
354     hideHints(HIDE_BY_OTHER_HINT, false, false);
355
356     final JBPopup popup =
357       JBPopupFactory.getInstance().createComponentPopupBuilder(component, null).setRequestFocus(false).setResizable(false).setMovable(false)
358         .createPopup();
359     popup.show(p);
360
361     ListenerUtil.addMouseListener(component, new MouseAdapter() {
362       @Override
363       public void mousePressed(MouseEvent e) {
364         myHideAlarm.cancelAllRequests();
365       }
366     });
367     ListenerUtil.addFocusListener(component, new FocusAdapter() {
368       @Override
369       public void focusGained(FocusEvent e) {
370         myHideAlarm.cancelAllRequests();
371       }
372     });
373
374     final HintInfo info = new HintInfo(new LightweightHint(component) {
375       @Override
376       public void hide() {
377         popup.cancel();
378       }
379     }, flags, false);
380     myHintsStack.add(info);
381     if (timeout > 0) {
382       Timer timer = UIUtil.createNamedTimer("Popup timeout",timeout, new ActionListener() {
383         @Override
384         public void actionPerformed(ActionEvent event) {
385           popup.dispose();
386         }
387       });
388       timer.setRepeats(false);
389       timer.start();
390     }
391   }
392
393   private static void doShowInGivenLocation(final LightweightHint hint, final Editor editor, Point p, HintHint hintInfo, boolean updateSize) {
394     if (ApplicationManager.getApplication().isUnitTestMode()) return;
395     JLayeredPane layeredPane = editor.getComponent().getRootPane().getLayeredPane();
396     Dimension size = updateSize ? hint.getComponent().getPreferredSize() : hint.getComponent().getSize();
397
398     if (hint.isRealPopup()) {
399       final Point editorCorner = editor.getComponent().getLocation();
400       SwingUtilities.convertPointToScreen(editorCorner, layeredPane);
401       final Point point = new Point(p);
402       SwingUtilities.convertPointToScreen(point, layeredPane);
403       final Rectangle editorScreen = ScreenUtil.getScreenRectangle(point.x, point.y);
404
405       SwingUtilities.convertPointToScreen(p, layeredPane);
406       final Rectangle rectangle = new Rectangle(p, size);
407       ScreenUtil.moveToFit(rectangle, editorScreen, null);
408       p = rectangle.getLocation();
409       SwingUtilities.convertPointFromScreen(p, layeredPane);
410     }
411     else if (layeredPane.getWidth() < p.x + size.width && !hintInfo.isAwtTooltip()) {
412       p.x = Math.max(0, layeredPane.getWidth() - size.width);
413     }
414
415     if (hint.isVisible()) {
416       if (updateSize) {
417         hint.updateBounds(p.x, p.y);
418       } else {
419         hint.updateLocation(p.x, p.y);
420       }
421     }
422     else {
423       hint.show(layeredPane, p.x, p.y, editor.getContentComponent(), hintInfo);
424     }
425   }
426
427   public static void updateLocation(final LightweightHint hint, final Editor editor, Point p) {
428     doShowInGivenLocation(hint, editor, p, createHintHint(editor, p, hint, UNDER), false);
429   }
430
431   public static void adjustEditorHintPosition(final LightweightHint hint, final Editor editor, final Point p, @PositionFlags short constraint) {
432     doShowInGivenLocation(hint, editor, p, createHintHint(editor, p, hint, constraint), true);
433   }
434
435   @Override
436   public void hideAllHints() {
437     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
438     for (HintInfo info : getHintsStackArray()) {
439       if (!info.hint.vetoesHiding()) {
440         info.hint.hide();
441       }
442     }
443     cleanup();
444   }
445
446   public void cleanup() {
447     myHintsStack.clear();
448     updateLastEditor(null);
449   }
450
451   /**
452    * @return coordinates in layered pane coordinate system.
453    */
454   public Point getHintPosition(@NotNull LightweightHint hint, @NotNull Editor editor, @PositionFlags short constraint) {
455
456     LogicalPosition pos = editor.getCaretModel().getLogicalPosition();
457     final DataContext dataContext = ((EditorEx)editor).getDataContext();
458     final Rectangle dominantArea = PlatformDataKeys.DOMINANT_HINT_AREA_RECTANGLE.getData(dataContext);
459
460     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
461     JRootPane rootPane = editor.getComponent().getRootPane();
462     if (dominantArea == null && rootPane != null) {
463       JLayeredPane lp = rootPane.getLayeredPane();
464       for (HintInfo info : getHintsStackArray()) {
465         if (!info.hint.isSelectingHint()) continue;
466         IdeTooltip tooltip = info.hint.getCurrentIdeTooltip();
467         if (tooltip != null) {
468           Point p = tooltip.getShowingPoint().getPoint(lp);
469           if (info.hint != hint) {
470             switch (constraint) {
471               case ABOVE:
472                 if (tooltip.getPreferredPosition() == Balloon.Position.below) {
473                   p.y -= tooltip.getPositionChangeY();
474                 }
475                 break;
476               case UNDER:
477               case RIGHT_UNDER:
478                 if (tooltip.getPreferredPosition() == Balloon.Position.above) {
479                   p.y += tooltip.getPositionChangeY();
480                 }
481                 break;
482               case RIGHT:
483                 if (tooltip.getPreferredPosition() == Balloon.Position.atLeft) {
484                   p.x += tooltip.getPositionChangeX();
485                 }
486                 break;
487               case LEFT:
488                 if (tooltip.getPreferredPosition() == Balloon.Position.atRight) {
489                   p.x -= tooltip.getPositionChangeX();
490                 }
491                 break;
492             }
493           }
494           return p;
495         }
496
497         Rectangle rectangle = info.hint.getBounds();
498         JComponent c = info.hint.getComponent();
499         rectangle = SwingUtilities.convertRectangle(c.getParent(), rectangle, lp);
500
501         if (rectangle != null) {
502           return getHintPositionRelativeTo(hint, editor, constraint, rectangle, pos);
503         }
504       }
505     }
506     else {
507       return getHintPositionRelativeTo(hint, editor, constraint, dominantArea, pos);
508     }
509
510     return getHintPosition(hint, editor, pos, constraint);
511   }
512
513   private static Point getHintPositionRelativeTo(final LightweightHint hint,
514                                                  final Editor editor,
515                                                  @PositionFlags  short constraint,
516                                                  final Rectangle lookupBounds,
517                                                  final LogicalPosition pos) {
518
519     JComponent editorComponent = editor.getComponent();
520     JLayeredPane layeredPane = editorComponent.getRootPane().getLayeredPane();
521
522     IdeTooltip ideTooltip = hint.getCurrentIdeTooltip();
523     if (ideTooltip != null) {
524       Point point = ideTooltip.getPoint();
525       return SwingUtilities.convertPoint(ideTooltip.getComponent(), point, layeredPane);
526     }
527
528     Dimension hintSize = hint.getComponent().getPreferredSize();
529     int layeredPaneHeight = layeredPane.getHeight();
530
531     switch (constraint) {
532       case LEFT: {
533         int y = lookupBounds.y;
534         if (y < 0) {
535           y = 0;
536         }
537         else if (y + hintSize.height >= layeredPaneHeight) {
538           y = layeredPaneHeight - hintSize.height;
539         }
540         return new Point(lookupBounds.x - hintSize.width, y);
541       }
542
543       case RIGHT: {
544         int y = lookupBounds.y;
545         if (y < 0) {
546           y = 0;
547         }
548         else if (y + hintSize.height >= layeredPaneHeight) {
549           y = layeredPaneHeight - hintSize.height;
550         }
551         return new Point(lookupBounds.x + lookupBounds.width, y);
552       }
553
554       case ABOVE:
555         Point posAboveCaret = getHintPosition(hint, editor, pos, ABOVE);
556         return new Point(lookupBounds.x, Math.min(posAboveCaret.y, lookupBounds.y - hintSize.height));
557
558       case UNDER:
559         Point posUnderCaret = getHintPosition(hint, editor, pos, UNDER);
560         return new Point(lookupBounds.x, Math.max(posUnderCaret.y, lookupBounds.y + lookupBounds.height));
561
562       default:
563         LOG.assertTrue(false);
564         return null;
565     }
566   }
567
568   /**
569    * @return position of hint in layered pane coordinate system
570    */
571   public static Point getHintPosition(@NotNull LightweightHint hint,
572                                       @NotNull Editor editor,
573                                       @NotNull LogicalPosition pos,
574                                       @PositionFlags short constraint) {
575     return getHintPosition(hint, editor, pos, pos, constraint);
576   }
577
578   private static Point getHintPosition(@NotNull LightweightHint hint,
579                                        @NotNull Editor editor,
580                                        @NotNull LogicalPosition pos1,
581                                        @NotNull LogicalPosition pos2,
582                                        @PositionFlags short constraint) {
583     return getHintPosition(hint, editor, pos1, pos2, constraint, Registry.is("editor.balloonHints"));
584   }
585
586   private static Point getHintPosition(@NotNull LightweightHint hint,
587                                        @NotNull Editor editor,
588                                        @NotNull LogicalPosition pos1,
589                                        @NotNull LogicalPosition pos2,
590                                        @PositionFlags short constraint,
591                                        boolean showByBalloon) {
592     if (ApplicationManager.getApplication().isUnitTestMode()) return new Point();
593     Point p = _getHintPosition(hint, editor, pos1, pos2, constraint, showByBalloon);
594     JLayeredPane layeredPane = editor.getComponent().getRootPane().getLayeredPane();
595     Dimension hintSize = hint.getComponent().getPreferredSize();
596     if (constraint == ABOVE) {
597       if (p.y < 0) {
598         Point p1 = _getHintPosition(hint, editor, pos1, pos2, UNDER, showByBalloon);
599         if (p1.y + hintSize.height <= layeredPane.getSize().height) {
600           return p1;
601         }
602       }
603     }
604     else if (constraint == UNDER) {
605       if (p.y + hintSize.height > layeredPane.getSize().height) {
606         Point p1 = _getHintPosition(hint, editor, pos1, pos2, ABOVE, showByBalloon);
607         if (p1.y >= 0) {
608           return p1;
609         }
610       }
611     }
612
613     return p;
614   }
615
616   private static Point _getHintPosition(@NotNull LightweightHint hint,
617                                         @NotNull Editor editor,
618                                         @NotNull LogicalPosition pos1,
619                                         @NotNull LogicalPosition pos2,
620                                         @PositionFlags short constraint,
621                                         boolean showByBalloon) {
622     Dimension hintSize = hint.getComponent().getPreferredSize();
623     int line1 = pos1.line;
624     int col1 = pos1.column;
625     int line2 = pos2.line;
626     int col2 = pos2.column;
627
628     Point location;
629     JLayeredPane layeredPane = editor.getComponent().getRootPane().getLayeredPane();
630     JComponent internalComponent = editor.getContentComponent();
631     if (constraint == RIGHT_UNDER) {
632       Point p = editor.logicalPositionToXY(new LogicalPosition(line2, col2));
633       if (!showByBalloon) {
634         p.y += editor.getLineHeight();
635       }
636       location = SwingUtilities.convertPoint(internalComponent, p, layeredPane);
637     }
638     else {
639       Point p = editor.logicalPositionToXY(new LogicalPosition(line1, col1));
640       if (constraint == UNDER) {
641         p.y += editor.getLineHeight();
642       }
643       location = SwingUtilities.convertPoint(internalComponent, p, layeredPane);
644     }
645
646     if (constraint == ABOVE && !showByBalloon) {
647       location.y -= hintSize.height;
648       int diff = location.x + hintSize.width - layeredPane.getWidth();
649       if (diff > 0) {
650         location.x = Math.max(location.x - diff, 0);
651       }
652     }
653
654     if ((constraint == LEFT || constraint == RIGHT) && !showByBalloon) {
655       location.y -= hintSize.height / 2;
656       if (constraint == LEFT) {
657         location.x -= hintSize.width;
658       }
659     }
660
661     return location;
662   }
663
664   @Override
665   public void showErrorHint(@NotNull Editor editor, @NotNull String text) {
666     showErrorHint(editor, text, ABOVE);
667   }
668
669   @Override
670   public void showErrorHint(@NotNull Editor editor, @NotNull String text, short position) {
671     JComponent label = HintUtil.createErrorLabel(text);
672     LightweightHint hint = new LightweightHint(label);
673     Point p = getHintPosition(hint, editor, position);
674     showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | HIDE_BY_SCROLLING, 0, false, position);
675   }
676
677   @Override
678   public void showInformationHint(@NotNull Editor editor, @NotNull String text) {
679     JComponent label = HintUtil.createInformationLabel(text);
680     showInformationHint(editor, label);
681   }
682
683   @Override
684   public void showInformationHint(@NotNull Editor editor, @NotNull JComponent component) {
685     showInformationHint(editor, component, true);
686   }
687
688   public void showInformationHint(@NotNull Editor editor, @NotNull JComponent component, boolean showByBalloon) {
689     if (ApplicationManager.getApplication().isUnitTestMode()) {
690       return;
691     }
692     LightweightHint hint = new LightweightHint(component);
693     Point p = getHintPosition(hint, editor, ABOVE);
694     showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | HIDE_BY_SCROLLING, 0, false);
695   }
696
697   @Override
698   public void showErrorHint(@NotNull Editor editor,
699                             @NotNull String hintText,
700                             int offset1,
701                             int offset2,
702                             short constraint,
703                             int flags,
704                             int timeout) {
705     JComponent label = HintUtil.createErrorLabel(hintText);
706     LightweightHint hint = new LightweightHint(label);
707     final LogicalPosition pos1 = editor.offsetToLogicalPosition(offset1);
708     final LogicalPosition pos2 = editor.offsetToLogicalPosition(offset2);
709     final Point p = getHintPosition(hint, editor, pos1, pos2, constraint);
710     showEditorHint(hint, editor, p, flags, timeout, false);
711   }
712
713
714   @Override
715   public void showQuestionHint(@NotNull Editor editor, @NotNull String hintText, int offset1, int offset2, @NotNull QuestionAction action) {
716
717     JComponent label = HintUtil.createQuestionLabel(hintText);
718     LightweightHint hint = new LightweightHint(label);
719     showQuestionHint(editor, offset1, offset2, hint, action, ABOVE);
720   }
721
722   public void showQuestionHint(@NotNull final Editor editor,
723                                final int offset1,
724                                final int offset2,
725                                @NotNull final LightweightHint hint,
726                                @NotNull final QuestionAction action,
727                                @PositionFlags short constraint) {
728     final LogicalPosition pos1 = editor.offsetToLogicalPosition(offset1);
729     final LogicalPosition pos2 = editor.offsetToLogicalPosition(offset2);
730     final Point p = getHintPosition(hint, editor, pos1, pos2, constraint);
731     showQuestionHint(editor, p, offset1, offset2, hint, action, constraint);
732   }
733
734
735   public void showQuestionHint(@NotNull final Editor editor,
736                                @NotNull final Point p,
737                                final int offset1,
738                                final int offset2,
739                                @NotNull final LightweightHint hint,
740                                @NotNull final QuestionAction action,
741                                @PositionFlags short constraint) {
742     TextAttributes attributes = new TextAttributes();
743     attributes.setEffectColor(HintUtil.QUESTION_UNDERSCORE_COLOR);
744     attributes.setEffectType(EffectType.LINE_UNDERSCORE);
745     final RangeHighlighter highlighter = editor.getMarkupModel()
746       .addRangeHighlighter(offset1, offset2, HighlighterLayer.ERROR + 1, attributes, HighlighterTargetArea.EXACT_RANGE);
747     if (myQuestionHint != null) {
748       myQuestionHint.hide();
749       myQuestionHint = null;
750       myQuestionAction = null;
751     }
752
753     hint.addHintListener(new HintListener() {
754       @Override
755       public void hintHidden(EventObject event) {
756         highlighter.dispose();
757
758         if (myQuestionHint == hint) {
759           myQuestionAction = null;
760           myQuestionHint = null;
761         }
762         hint.removeHintListener(this);
763       }
764     });
765
766     showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | UPDATE_BY_SCROLLING | HIDE_IF_OUT_OF_EDITOR, 0, false,
767                    createHintHint(editor, p, hint, constraint));
768     myQuestionAction = action;
769     myQuestionHint = hint;
770   }
771
772   public static HintHint createHintHint(Editor editor, Point p, LightweightHint hint, @PositionFlags short constraint) {
773     return createHintHint(editor, p, hint, constraint, false);
774   }
775
776   //todo[nik,kirillk] perhaps 'createInEditorComponent' parameter should always be 'true'
777   //old 'createHintHint' method uses LayeredPane as original component for HintHint so IdeTooltipManager.eventDispatched()
778   //wasn't able to correctly hide tooltip after mouse move.
779   public static HintHint createHintHint(Editor editor, Point p, LightweightHint hint, @PositionFlags short constraint, boolean createInEditorComponent) {
780     JRootPane rootPane = editor.getComponent().getRootPane();
781     if (rootPane == null) {
782       return new HintHint(editor, p);
783     }
784
785     JLayeredPane lp = rootPane.getLayeredPane();
786     HintHint hintInfo = new HintHint(editor, SwingUtilities.convertPoint(lp, p, editor.getContentComponent()));
787     boolean showByBalloon = Registry.is("editor.balloonHints");
788     if (showByBalloon) {
789       if (!createInEditorComponent) {
790         hintInfo = new HintHint(lp, p);
791       }
792       hintInfo.setAwtTooltip(true).setHighlighterType(true);
793     }
794
795
796     hintInfo.initStyleFrom(hint.getComponent());
797     if (showByBalloon) {
798       hintInfo.setBorderColor(new JBColor(Color.gray, Gray._140));
799       hintInfo.setFont(hintInfo.getTextFont().deriveFont(Font.PLAIN));
800       hintInfo.setCalloutShift((int)(editor.getLineHeight() * 0.1));
801     }
802     hintInfo.setPreferredPosition(Balloon.Position.above);
803     if (constraint == UNDER || constraint == RIGHT_UNDER) {
804       hintInfo.setPreferredPosition(Balloon.Position.below);
805     }
806     else if (constraint == RIGHT) {
807       hintInfo.setPreferredPosition(Balloon.Position.atRight);
808     }
809     else if (constraint == LEFT) {
810       hintInfo.setPreferredPosition(Balloon.Position.atLeft);
811     }
812
813     if (hint.isAwtTooltip()) {
814       hintInfo.setAwtTooltip(true);
815     }
816
817     hintInfo.setPositionChangeShift(0, editor.getLineHeight());
818
819     return hintInfo;
820   }
821
822   private void updateLastEditor(final Editor editor) {
823     if (myLastEditor != editor) {
824       if (myLastEditor != null) {
825         myLastEditor.removeEditorMouseListener(myEditorMouseListener);
826         myLastEditor.getContentComponent().removeFocusListener(myEditorFocusListener);
827         myLastEditor.getDocument().removeDocumentListener(myEditorDocumentListener);
828         myLastEditor.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener);
829         myLastEditor.getCaretModel().removeCaretListener(myCaretMoveListener);
830       }
831
832       myLastEditor = editor;
833       if (myLastEditor != null) {
834         myLastEditor.addEditorMouseListener(myEditorMouseListener);
835         myLastEditor.getContentComponent().addFocusListener(myEditorFocusListener);
836         myLastEditor.getDocument().addDocumentListener(myEditorDocumentListener);
837         myLastEditor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener);
838         myLastEditor.getCaretModel().addCaretListener(myCaretMoveListener);
839       }
840     }
841   }
842
843   private class MyAnActionListener implements AnActionListener {
844     @Override
845     public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
846       if (action instanceof ActionToIgnore) return;
847
848       AnAction escapeAction = ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_EDITOR_ESCAPE);
849       if (action == escapeAction) return;
850
851       hideHints(HIDE_BY_ANY_KEY, false, false);
852     }
853
854
855     @Override
856     public void afterActionPerformed(final AnAction action, final DataContext dataContext, AnActionEvent event) {
857     }
858
859     @Override
860     public void beforeEditorTyping(char c, DataContext dataContext) {
861     }
862   }
863
864   /**
865    * Hides all hints when selected editor changes. Unfortunately  user can change
866    * selected editor by mouse. These clicks are not AnActions so they are not
867    * fired by ActionManager.
868    */
869   private final class MyEditorManagerListener extends FileEditorManagerAdapter {
870     @Override
871     public void selectionChanged(@NotNull FileEditorManagerEvent event) {
872       hideHints(0, false, true);
873     }
874   }
875
876   /**
877    * We have to spy for all opened projects to register MyEditorManagerListener into
878    * all opened projects.
879    */
880   private final class MyProjectManagerListener extends ProjectManagerAdapter {
881     @Override
882     public void projectOpened(Project project) {
883       project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myEditorManagerListener);
884     }
885
886     @Override
887     public void projectClosed(Project project) {
888       // avoid leak through com.intellij.codeInsight.hint.TooltipController.myCurrentTooltip
889       TooltipController.getInstance().cancelTooltips();
890
891       myQuestionAction = null;
892       myQuestionHint = null;
893     }
894   }
895
896   boolean isEscapeHandlerEnabled() {
897     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
898     for (int i = myHintsStack.size() - 1; i >= 0; i--) {
899       final HintInfo info = myHintsStack.get(i);
900       if (!info.hint.isVisible()) {
901         myHintsStack.remove(i);
902
903         // We encountered situation when 'hint' instances use 'hide()' method as object destruction callback
904         // (e.g. LineTooltipRenderer creates hint that overrides keystroke of particular action that produces hint and
905         // de-registers it inside 'hide()'. That means that the hint can 'stuck' to old editor location if we just remove
906         // it but don't call hide())
907         info.hint.hide();
908         continue;
909       }
910
911       if ((info.flags & (HIDE_BY_ESCAPE | HIDE_BY_ANY_KEY)) != 0) {
912         return true;
913       }
914     }
915     return false;
916   }
917
918   @Override
919   public boolean hideHints(int mask, boolean onlyOne, boolean editorChanged) {
920     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
921     try {
922       boolean done = false;
923
924       for (int i = myHintsStack.size() - 1; i >= 0; i--) {
925         final HintInfo info = myHintsStack.get(i);
926         if (!info.hint.isVisible() && !info.hint.vetoesHiding()) {
927           myHintsStack.remove(i);
928
929           // We encountered situation when 'hint' instances use 'hide()' method as object destruction callback
930           // (e.g. LineTooltipRenderer creates hint that overrides keystroke of particular action that produces hint and
931           // de-registers it inside 'hide()'. That means that the hint can 'stuck' to old editor location if we just remove
932           // it but don't call hide())
933           info.hint.hide();
934           continue;
935         }
936
937         if ((info.flags & mask) != 0 || editorChanged && !info.reviveOnEditorChange) {
938           info.hint.hide();
939           myHintsStack.remove(info);
940           if (onlyOne) {
941             return true;
942           }
943           done = true;
944         }
945       }
946
947       return done;
948     }
949     finally {
950       if (myHintsStack.isEmpty()) {
951         updateLastEditor(null);
952       }
953     }
954   }
955
956   private static class EditorHintListenerHolder {
957     private static final EditorHintListener ourEditorHintPublisher =
958       ApplicationManager.getApplication().getMessageBus().syncPublisher(EditorHintListener.TOPIC);
959
960     private EditorHintListenerHolder() {
961     }
962   }
963
964   private static EditorHintListener getPublisher() {
965     return EditorHintListenerHolder.ourEditorHintPublisher;
966   }
967 }