https://ea.jetbrains.com/browser/ea_problems/60655
[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     @NotNull JComponent externalComponent = editor.getComponent();
630     JRootPane rootPane = externalComponent.getRootPane();
631     if (rootPane != null) {
632       externalComponent = rootPane;
633       JLayeredPane layeredPane = rootPane.getLayeredPane();
634       if (layeredPane != null) {
635         externalComponent = layeredPane;
636       }
637     }
638     JComponent internalComponent = editor.getContentComponent();
639     if (constraint == RIGHT_UNDER) {
640       Point p = editor.logicalPositionToXY(new LogicalPosition(line2, col2));
641       if (!showByBalloon) {
642         p.y += editor.getLineHeight();
643       }
644       location = SwingUtilities.convertPoint(internalComponent, p, externalComponent);
645     }
646     else {
647       Point p = editor.logicalPositionToXY(new LogicalPosition(line1, col1));
648       if (constraint == UNDER) {
649         p.y += editor.getLineHeight();
650       }
651       location = SwingUtilities.convertPoint(internalComponent, p, externalComponent);
652     }
653
654     if (constraint == ABOVE && !showByBalloon) {
655       location.y -= hintSize.height;
656       int diff = location.x + hintSize.width - externalComponent.getWidth();
657       if (diff > 0) {
658         location.x = Math.max(location.x - diff, 0);
659       }
660     }
661
662     if ((constraint == LEFT || constraint == RIGHT) && !showByBalloon) {
663       location.y -= hintSize.height / 2;
664       if (constraint == LEFT) {
665         location.x -= hintSize.width;
666       }
667     }
668
669     return location;
670   }
671
672   @Override
673   public void showErrorHint(@NotNull Editor editor, @NotNull String text) {
674     showErrorHint(editor, text, ABOVE);
675   }
676
677   @Override
678   public void showErrorHint(@NotNull Editor editor, @NotNull String text, short position) {
679     JComponent label = HintUtil.createErrorLabel(text);
680     LightweightHint hint = new LightweightHint(label);
681     Point p = getHintPosition(hint, editor, position);
682     showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | HIDE_BY_SCROLLING, 0, false, position);
683   }
684
685   @Override
686   public void showInformationHint(@NotNull Editor editor, @NotNull String text) {
687     JComponent label = HintUtil.createInformationLabel(text);
688     showInformationHint(editor, label);
689   }
690
691   @Override
692   public void showInformationHint(@NotNull Editor editor, @NotNull JComponent component) {
693     showInformationHint(editor, component, true);
694   }
695
696   public void showInformationHint(@NotNull Editor editor, @NotNull JComponent component, boolean showByBalloon) {
697     if (ApplicationManager.getApplication().isUnitTestMode()) {
698       return;
699     }
700     LightweightHint hint = new LightweightHint(component);
701     Point p = getHintPosition(hint, editor, ABOVE);
702     showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | HIDE_BY_SCROLLING, 0, false);
703   }
704
705   @Override
706   public void showErrorHint(@NotNull Editor editor,
707                             @NotNull String hintText,
708                             int offset1,
709                             int offset2,
710                             short constraint,
711                             int flags,
712                             int timeout) {
713     JComponent label = HintUtil.createErrorLabel(hintText);
714     LightweightHint hint = new LightweightHint(label);
715     final LogicalPosition pos1 = editor.offsetToLogicalPosition(offset1);
716     final LogicalPosition pos2 = editor.offsetToLogicalPosition(offset2);
717     final Point p = getHintPosition(hint, editor, pos1, pos2, constraint);
718     showEditorHint(hint, editor, p, flags, timeout, false);
719   }
720
721
722   @Override
723   public void showQuestionHint(@NotNull Editor editor, @NotNull String hintText, int offset1, int offset2, @NotNull QuestionAction action) {
724
725     JComponent label = HintUtil.createQuestionLabel(hintText);
726     LightweightHint hint = new LightweightHint(label);
727     showQuestionHint(editor, offset1, offset2, hint, action, ABOVE);
728   }
729
730   public void showQuestionHint(@NotNull final Editor editor,
731                                final int offset1,
732                                final int offset2,
733                                @NotNull final LightweightHint hint,
734                                @NotNull final QuestionAction action,
735                                @PositionFlags short constraint) {
736     final LogicalPosition pos1 = editor.offsetToLogicalPosition(offset1);
737     final LogicalPosition pos2 = editor.offsetToLogicalPosition(offset2);
738     final Point p = getHintPosition(hint, editor, pos1, pos2, constraint);
739     showQuestionHint(editor, p, offset1, offset2, hint, action, constraint);
740   }
741
742
743   public void showQuestionHint(@NotNull final Editor editor,
744                                @NotNull final Point p,
745                                final int offset1,
746                                final int offset2,
747                                @NotNull final LightweightHint hint,
748                                @NotNull final QuestionAction action,
749                                @PositionFlags short constraint) {
750     TextAttributes attributes = new TextAttributes();
751     attributes.setEffectColor(HintUtil.QUESTION_UNDERSCORE_COLOR);
752     attributes.setEffectType(EffectType.LINE_UNDERSCORE);
753     final RangeHighlighter highlighter = editor.getMarkupModel()
754       .addRangeHighlighter(offset1, offset2, HighlighterLayer.ERROR + 1, attributes, HighlighterTargetArea.EXACT_RANGE);
755     if (myQuestionHint != null) {
756       myQuestionHint.hide();
757       myQuestionHint = null;
758       myQuestionAction = null;
759     }
760
761     hint.addHintListener(new HintListener() {
762       @Override
763       public void hintHidden(EventObject event) {
764         highlighter.dispose();
765
766         if (myQuestionHint == hint) {
767           myQuestionAction = null;
768           myQuestionHint = null;
769         }
770         hint.removeHintListener(this);
771       }
772     });
773
774     showEditorHint(hint, editor, p, HIDE_BY_ANY_KEY | HIDE_BY_TEXT_CHANGE | UPDATE_BY_SCROLLING | HIDE_IF_OUT_OF_EDITOR, 0, false,
775                    createHintHint(editor, p, hint, constraint));
776     myQuestionAction = action;
777     myQuestionHint = hint;
778   }
779
780   public static HintHint createHintHint(Editor editor, Point p, LightweightHint hint, @PositionFlags short constraint) {
781     return createHintHint(editor, p, hint, constraint, false);
782   }
783
784   //todo[nik,kirillk] perhaps 'createInEditorComponent' parameter should always be 'true'
785   //old 'createHintHint' method uses LayeredPane as original component for HintHint so IdeTooltipManager.eventDispatched()
786   //wasn't able to correctly hide tooltip after mouse move.
787   public static HintHint createHintHint(Editor editor, Point p, LightweightHint hint, @PositionFlags short constraint, boolean createInEditorComponent) {
788     JRootPane rootPane = editor.getComponent().getRootPane();
789     if (rootPane == null) {
790       return new HintHint(editor, p);
791     }
792
793     JLayeredPane lp = rootPane.getLayeredPane();
794     HintHint hintInfo = new HintHint(editor, SwingUtilities.convertPoint(lp, p, editor.getContentComponent()));
795     boolean showByBalloon = Registry.is("editor.balloonHints");
796     if (showByBalloon) {
797       if (!createInEditorComponent) {
798         hintInfo = new HintHint(lp, p);
799       }
800       hintInfo.setAwtTooltip(true).setHighlighterType(true);
801     }
802
803
804     hintInfo.initStyleFrom(hint.getComponent());
805     if (showByBalloon) {
806       hintInfo.setBorderColor(new JBColor(Color.gray, Gray._140));
807       hintInfo.setFont(hintInfo.getTextFont().deriveFont(Font.PLAIN));
808       hintInfo.setCalloutShift((int)(editor.getLineHeight() * 0.1));
809     }
810     hintInfo.setPreferredPosition(Balloon.Position.above);
811     if (constraint == UNDER || constraint == RIGHT_UNDER) {
812       hintInfo.setPreferredPosition(Balloon.Position.below);
813     }
814     else if (constraint == RIGHT) {
815       hintInfo.setPreferredPosition(Balloon.Position.atRight);
816     }
817     else if (constraint == LEFT) {
818       hintInfo.setPreferredPosition(Balloon.Position.atLeft);
819     }
820
821     if (hint.isAwtTooltip()) {
822       hintInfo.setAwtTooltip(true);
823     }
824
825     hintInfo.setPositionChangeShift(0, editor.getLineHeight());
826
827     return hintInfo;
828   }
829
830   private void updateLastEditor(final Editor editor) {
831     if (myLastEditor != editor) {
832       if (myLastEditor != null) {
833         myLastEditor.removeEditorMouseListener(myEditorMouseListener);
834         myLastEditor.getContentComponent().removeFocusListener(myEditorFocusListener);
835         myLastEditor.getDocument().removeDocumentListener(myEditorDocumentListener);
836         myLastEditor.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener);
837         myLastEditor.getCaretModel().removeCaretListener(myCaretMoveListener);
838       }
839
840       myLastEditor = editor;
841       if (myLastEditor != null) {
842         myLastEditor.addEditorMouseListener(myEditorMouseListener);
843         myLastEditor.getContentComponent().addFocusListener(myEditorFocusListener);
844         myLastEditor.getDocument().addDocumentListener(myEditorDocumentListener);
845         myLastEditor.getScrollingModel().addVisibleAreaListener(myVisibleAreaListener);
846         myLastEditor.getCaretModel().addCaretListener(myCaretMoveListener);
847       }
848     }
849   }
850
851   private class MyAnActionListener implements AnActionListener {
852     @Override
853     public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
854       if (action instanceof ActionToIgnore) return;
855
856       AnAction escapeAction = ActionManagerEx.getInstanceEx().getAction(IdeActions.ACTION_EDITOR_ESCAPE);
857       if (action == escapeAction) return;
858
859       hideHints(HIDE_BY_ANY_KEY, false, false);
860     }
861
862
863     @Override
864     public void afterActionPerformed(final AnAction action, final DataContext dataContext, AnActionEvent event) {
865     }
866
867     @Override
868     public void beforeEditorTyping(char c, DataContext dataContext) {
869     }
870   }
871
872   /**
873    * Hides all hints when selected editor changes. Unfortunately  user can change
874    * selected editor by mouse. These clicks are not AnActions so they are not
875    * fired by ActionManager.
876    */
877   private final class MyEditorManagerListener extends FileEditorManagerAdapter {
878     @Override
879     public void selectionChanged(@NotNull FileEditorManagerEvent event) {
880       hideHints(0, false, true);
881     }
882   }
883
884   /**
885    * We have to spy for all opened projects to register MyEditorManagerListener into
886    * all opened projects.
887    */
888   private final class MyProjectManagerListener extends ProjectManagerAdapter {
889     @Override
890     public void projectOpened(Project project) {
891       project.getMessageBus().connect(project).subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, myEditorManagerListener);
892     }
893
894     @Override
895     public void projectClosed(Project project) {
896       // avoid leak through com.intellij.codeInsight.hint.TooltipController.myCurrentTooltip
897       TooltipController.getInstance().cancelTooltips();
898
899       myQuestionAction = null;
900       myQuestionHint = null;
901     }
902   }
903
904   boolean isEscapeHandlerEnabled() {
905     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
906     for (int i = myHintsStack.size() - 1; i >= 0; i--) {
907       final HintInfo info = myHintsStack.get(i);
908       if (!info.hint.isVisible()) {
909         myHintsStack.remove(i);
910
911         // We encountered situation when 'hint' instances use 'hide()' method as object destruction callback
912         // (e.g. LineTooltipRenderer creates hint that overrides keystroke of particular action that produces hint and
913         // de-registers it inside 'hide()'. That means that the hint can 'stuck' to old editor location if we just remove
914         // it but don't call hide())
915         info.hint.hide();
916         continue;
917       }
918
919       if ((info.flags & (HIDE_BY_ESCAPE | HIDE_BY_ANY_KEY)) != 0) {
920         return true;
921       }
922     }
923     return false;
924   }
925
926   @Override
927   public boolean hideHints(int mask, boolean onlyOne, boolean editorChanged) {
928     LOG.assertTrue(SwingUtilities.isEventDispatchThread());
929     try {
930       boolean done = false;
931
932       for (int i = myHintsStack.size() - 1; i >= 0; i--) {
933         final HintInfo info = myHintsStack.get(i);
934         if (!info.hint.isVisible() && !info.hint.vetoesHiding()) {
935           myHintsStack.remove(i);
936
937           // We encountered situation when 'hint' instances use 'hide()' method as object destruction callback
938           // (e.g. LineTooltipRenderer creates hint that overrides keystroke of particular action that produces hint and
939           // de-registers it inside 'hide()'. That means that the hint can 'stuck' to old editor location if we just remove
940           // it but don't call hide())
941           info.hint.hide();
942           continue;
943         }
944
945         if ((info.flags & mask) != 0 || editorChanged && !info.reviveOnEditorChange) {
946           info.hint.hide();
947           myHintsStack.remove(info);
948           if (onlyOne) {
949             return true;
950           }
951           done = true;
952         }
953       }
954
955       return done;
956     }
957     finally {
958       if (myHintsStack.isEmpty()) {
959         updateLastEditor(null);
960       }
961     }
962   }
963
964   private static class EditorHintListenerHolder {
965     private static final EditorHintListener ourEditorHintPublisher =
966       ApplicationManager.getApplication().getMessageBus().syncPublisher(EditorHintListener.TOPIC);
967
968     private EditorHintListenerHolder() {
969     }
970   }
971
972   private static EditorHintListener getPublisher() {
973     return EditorHintListenerHolder.ourEditorHintPublisher;
974   }
975 }