IDEA-141464 Debugger popup dissappears when trying to hover mouse on "+"
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / LightweightHint.java
1 /*
2  * Copyright 2000-2015 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.ui;
17
18 import com.intellij.codeInsight.hint.TooltipController;
19 import com.intellij.ide.IdeTooltip;
20 import com.intellij.ide.IdeTooltipManager;
21 import com.intellij.ide.TooltipEvent;
22 import com.intellij.openapi.diagnostic.Logger;
23 import com.intellij.openapi.ui.popup.Balloon;
24 import com.intellij.openapi.ui.popup.JBPopup;
25 import com.intellij.openapi.ui.popup.JBPopupFactory;
26 import com.intellij.openapi.util.Computable;
27 import com.intellij.openapi.util.UserDataHolderBase;
28 import com.intellij.openapi.wm.ex.LayoutFocusTraversalPolicyExt;
29 import com.intellij.ui.awt.RelativePoint;
30 import com.intellij.ui.components.panels.OpaquePanel;
31 import org.jetbrains.annotations.NotNull;
32
33 import javax.swing.*;
34 import javax.swing.border.Border;
35 import javax.swing.border.LineBorder;
36 import javax.swing.event.EventListenerList;
37 import java.awt.*;
38 import java.awt.event.ActionEvent;
39 import java.awt.event.ActionListener;
40 import java.awt.event.KeyEvent;
41 import java.awt.event.MouseEvent;
42 import java.util.EventListener;
43 import java.util.EventObject;
44
45 public class LightweightHint extends UserDataHolderBase implements Hint {
46   private static final Logger LOG = Logger.getInstance("#com.intellij.ui.LightweightHint");
47
48   private final JComponent myComponent;
49   private JComponent myFocusBackComponent;
50   private final EventListenerList myListenerList = new EventListenerList();
51   private MyEscListener myEscListener;
52   private JBPopup myPopup;
53   private JComponent myParentComponent;
54   private boolean myIsRealPopup = false;
55   private boolean myForceLightweightPopup = false;
56   private boolean mySelectingHint;
57
58   private boolean myForceShowAsPopup = false;
59   private String myTitle = null;
60   private boolean myCancelOnClickOutside = true;
61   private boolean myCancelOnOtherWindowOpen = true;
62   private boolean myResizable;
63
64   private IdeTooltip myCurrentIdeTooltip;
65   private HintHint myHintHint;
66   private JComponent myFocusRequestor;
67
68   private boolean myForceHideShadow = false;
69
70   public LightweightHint(@NotNull final JComponent component) {
71     myComponent = component;
72   }
73
74   public void setForceLightweightPopup(final boolean forceLightweightPopup) {
75     myForceLightweightPopup = forceLightweightPopup;
76   }
77
78
79   public void setForceShowAsPopup(final boolean forceShowAsPopup) {
80     myForceShowAsPopup = forceShowAsPopup;
81   }
82
83   public void setFocusRequestor(JComponent c) {
84     myFocusRequestor = c;
85   }
86
87   public void setTitle(final String title) {
88     myTitle = title;
89   }
90
91   public boolean isSelectingHint() {
92     return mySelectingHint;
93   }
94
95   public void setSelectingHint(final boolean selectingHint) {
96     mySelectingHint = selectingHint;
97   }
98
99   public void setCancelOnClickOutside(final boolean b) {
100     myCancelOnClickOutside = b;
101   }
102
103   public void setCancelOnOtherWindowOpen(final boolean b) {
104     myCancelOnOtherWindowOpen = b;
105   }
106
107   public void setResizable(final boolean b) {
108     myResizable = b;
109   }
110
111   protected boolean canAutoHideOn(TooltipEvent event) {
112     return true;
113   }
114
115   /**
116    * Shows the hint in the layered pane. Coordinates <code>x</code> and <code>y</code>
117    * are in <code>parentComponent</code> coordinate system. Note that the component
118    * appears on 250 layer.
119    */
120   @Override
121   public void show(@NotNull final JComponent parentComponent,
122                    final int x,
123                    final int y,
124                    final JComponent focusBackComponent,
125                    @NotNull final HintHint hintHint) {
126     myParentComponent = parentComponent;
127     myHintHint = hintHint;
128
129     myFocusBackComponent = focusBackComponent;
130
131     LOG.assertTrue(myParentComponent.isShowing());
132     myEscListener = new MyEscListener();
133     myComponent.registerKeyboardAction(myEscListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
134     myComponent.registerKeyboardAction(myEscListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_FOCUSED);
135     final JLayeredPane layeredPane = parentComponent.getRootPane().getLayeredPane();
136
137     myComponent.validate();
138
139     if (!myForceShowAsPopup &&
140         (myForceLightweightPopup ||
141          fitsLayeredPane(layeredPane, myComponent, new RelativePoint(parentComponent, new Point(x, y)), hintHint))) {
142       beforeShow();
143       final Dimension preferredSize = myComponent.getPreferredSize();
144
145
146       if (hintHint.isAwtTooltip()) {
147         IdeTooltip tooltip =
148           new IdeTooltip(hintHint.getOriginalComponent(), hintHint.getOriginalPoint(), myComponent, hintHint, myComponent) {
149             @Override
150             protected boolean canAutohideOn(TooltipEvent event) {
151               if (!LightweightHint.this.canAutoHideOn(event)) {
152                 return false;
153               }
154               else if (event.getInputEvent() instanceof MouseEvent) {
155                 return !(hintHint.isContentActive() && event.isIsEventInsideBalloon());
156               }
157               else if (event.getAction() != null) {
158                 return false;
159               }
160               else {
161                 return true;
162               }
163             }
164
165             @Override
166             protected void onHidden() {
167               fireHintHidden();
168               TooltipController.getInstance().resetCurrent();
169             }
170
171           @Override
172           public boolean canBeDismissedOnTimeout() {
173             return false;
174           }
175         }.setToCenterIfSmall(hintHint.isMayCenterTooltip())
176           .setPreferredPosition(hintHint.getPreferredPosition())
177           .setHighlighterType(hintHint.isHighlighterType())
178           .setTextForeground(hintHint.getTextForeground())
179           .setTextBackground(hintHint.getTextBackground())
180           .setBorderColor(hintHint.getBorderColor())
181           .setBorderInsets(hintHint.getBorderInsets())
182           .setFont(hintHint.getTextFont())
183           .setCalloutShift(hintHint.getCalloutShift())
184           .setPositionChangeShift(hintHint.getPositionChangeX(), hintHint.getPositionChangeY())
185           .setExplicitClose(hintHint.isExplicitClose())
186           .setHint(true);
187         myComponent.validate();
188         myCurrentIdeTooltip = IdeTooltipManager.getInstance().show(tooltip, hintHint.isShowImmediately(), hintHint.isAnimationEnabled());
189       }
190       else {
191         final Point layeredPanePoint = SwingUtilities.convertPoint(parentComponent, x, y, layeredPane);
192         myComponent.setBounds(layeredPanePoint.x, layeredPanePoint.y, preferredSize.width, preferredSize.height);
193         layeredPane.add(myComponent, JLayeredPane.POPUP_LAYER);
194
195         myComponent.validate();
196         myComponent.repaint();
197       }
198     }
199     else {
200       myIsRealPopup = true;
201       Point actualPoint = new Point(x, y);
202       JComponent actualComponent = new OpaquePanel(new BorderLayout());
203       actualComponent.add(myComponent, BorderLayout.CENTER);
204       if (isAwtTooltip()) {
205         int inset = BalloonImpl.getNormalInset();
206         actualComponent.setBorder(new LineBorder(hintHint.getTextBackground(), inset));
207         actualComponent.setBackground(hintHint.getTextBackground());
208         actualComponent.validate();
209       }
210
211       myPopup = JBPopupFactory.getInstance().createComponentPopupBuilder(actualComponent, myFocusRequestor)
212         .setRequestFocus(myFocusRequestor != null)
213         .setFocusable(myFocusRequestor != null)
214         .setResizable(myResizable)
215         .setMovable(myTitle != null)
216         .setTitle(myTitle)
217         .setModalContext(false)
218         .setShowShadow(isRealPopup() && !isForceHideShadow())
219         .setCancelKeyEnabled(false)
220         .setCancelOnClickOutside(myCancelOnClickOutside)
221         .setCancelCallback(new Computable<Boolean>() {
222           @Override
223           public Boolean compute() {
224             onPopupCancel();
225             return true;
226           }
227         })
228         .setCancelOnOtherWindowOpen(myCancelOnOtherWindowOpen)
229         .createPopup();
230
231       beforeShow();
232       myPopup.show(new RelativePoint(myParentComponent, new Point(actualPoint.x, actualPoint.y)));
233     }
234   }
235
236   protected void onPopupCancel() {
237   }
238
239   private void fixActualPoint(Point actualPoint) {
240     if (!isAwtTooltip()) return;
241     if (!myIsRealPopup) return;
242
243     Dimension size = myComponent.getPreferredSize();
244     Balloon.Position position = myHintHint.getPreferredPosition();
245     int shift = BalloonImpl.getPointerLength(position, false);
246     switch (position) {
247       case below:
248         actualPoint.y += shift;
249         break;
250       case above:
251         actualPoint.y -= (shift + size.height);
252         break;
253       case atLeft:
254         actualPoint.x -= (shift + size.width);
255         break;
256       case atRight:
257         actualPoint.y += shift;
258         break;
259     }
260   }
261
262   protected void beforeShow() {
263
264   }
265
266   public boolean vetoesHiding() {
267     return false;
268   }
269
270   public boolean isForceHideShadow() {
271     return myForceHideShadow;
272   }
273
274   public void setForceHideShadow(boolean forceHideShadow) {
275     myForceHideShadow = forceHideShadow;
276   }
277
278   private static boolean fitsLayeredPane(JLayeredPane pane, JComponent component, RelativePoint desiredLocation, HintHint hintHint) {
279     if (hintHint.isAwtTooltip()) {
280       Dimension size = component.getPreferredSize();
281       Dimension paneSize = pane.getSize();
282
283       Point target = desiredLocation.getPointOn(pane).getPoint();
284       Balloon.Position pos = hintHint.getPreferredPosition();
285       int pointer = BalloonImpl.getPointerLength(pos, false) + BalloonImpl.getNormalInset();
286       if (pos == Balloon.Position.above || pos == Balloon.Position.below) {
287         boolean heightFit = target.y - size.height - pointer > 0 || target.y + size.height + pointer < paneSize.height;
288         return heightFit && size.width + pointer < paneSize.width;
289       }
290       else {
291         boolean widthFit = target.x - size.width - pointer > 0 || target.x + size.width + pointer < paneSize.width;
292         return widthFit && size.height + pointer < paneSize.height;
293       }
294     }
295     else {
296       final Rectangle lpRect = new Rectangle(pane.getLocationOnScreen().x, pane.getLocationOnScreen().y, pane.getWidth(), pane.getHeight());
297       Rectangle componentRect = new Rectangle(desiredLocation.getScreenPoint().x,
298                                               desiredLocation.getScreenPoint().y,
299                                               component.getPreferredSize().width,
300                                               component.getPreferredSize().height);
301       return lpRect.contains(componentRect);
302     }
303   }
304
305   private void fireHintHidden() {
306     final EventListener[] listeners = myListenerList.getListeners(HintListener.class);
307     for (EventListener listener : listeners) {
308       ((HintListener)listener).hintHidden(new EventObject(this));
309     }
310   }
311
312   /**
313    * @return bounds of hint component in the layered pane.
314    */
315   public final Rectangle getBounds() {
316     return myComponent.getBounds();
317   }
318
319   @Override
320   public boolean isVisible() {
321     if (myIsRealPopup) {
322       return myPopup != null && myPopup.isVisible();
323     }
324     if (myCurrentIdeTooltip != null) {
325       return myComponent.isShowing() || IdeTooltipManager.getInstance().isQueuedToShow(myCurrentIdeTooltip);
326     }
327     return myComponent.isShowing();
328   }
329
330   public final boolean isRealPopup() {
331     return myIsRealPopup || myForceShowAsPopup;
332   }
333
334   @Override
335   public void hide() {
336     hide(false);
337   }
338
339   public void hide(boolean ok) {
340     if (isVisible()) {
341       if (myIsRealPopup) {
342         if (ok) {
343           myPopup.closeOk(null);
344         }
345         else {
346           myPopup.cancel();
347         }
348         myPopup = null;
349       }
350       else {
351         if (myCurrentIdeTooltip != null) {
352           IdeTooltip tooltip = myCurrentIdeTooltip;
353           myCurrentIdeTooltip = null;
354           tooltip.hide();
355         }
356         else {
357           final JRootPane rootPane = myComponent.getRootPane();
358           if (rootPane != null) {
359             final Rectangle bounds = myComponent.getBounds();
360             final JLayeredPane layeredPane = rootPane.getLayeredPane();
361
362             try {
363               if (myFocusBackComponent != null) {
364                 LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(myFocusBackComponent);
365               }
366               layeredPane.remove(myComponent);
367             }
368             finally {
369               LayoutFocusTraversalPolicyExt.setOverridenDefaultComponent(null);
370             }
371
372             layeredPane.paintImmediately(bounds.x, bounds.y, bounds.width, bounds.height);
373           }
374         }
375       }
376     }
377     if (myEscListener != null) {
378       myComponent.unregisterKeyboardAction(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
379     }
380
381     TooltipController.getInstance().hide(this);
382
383     fireHintHidden();
384   }
385
386   @Override
387   public void pack() {
388     setSize(myComponent.getPreferredSize());
389   }
390
391   @Override
392   public void updateBounds(int x, int y) {
393     setSize(myComponent.getPreferredSize());
394     updateLocation(x, y);
395   }
396
397   public void updateLocation(int x, int y) {
398     Point point = new Point(x, y);
399     fixActualPoint(point);
400     setLocation(new RelativePoint(myParentComponent, point));
401   }
402
403   public final JComponent getComponent() {
404     return myComponent;
405   }
406
407   @Override
408   public final void addHintListener(@NotNull final HintListener listener) {
409     myListenerList.add(HintListener.class, listener);
410   }
411
412   @Override
413   public final void removeHintListener(@NotNull final HintListener listener) {
414     myListenerList.remove(HintListener.class, listener);
415   }
416
417   public Point getLocationOn(JComponent c) {
418     Point location;
419     if (isRealPopup()) {
420       location = myPopup.getLocationOnScreen();
421       SwingUtilities.convertPointFromScreen(location, c);
422     }
423     else {
424       if (myCurrentIdeTooltip != null) {
425         Point tipPoint = myCurrentIdeTooltip.getPoint();
426         Component tipComponent = myCurrentIdeTooltip.getComponent();
427         return SwingUtilities.convertPoint(tipComponent, tipPoint, c);
428       }
429       else {
430         location = SwingUtilities.convertPoint(
431           myComponent.getParent(),
432           myComponent.getLocation(),
433           c
434         );
435       }
436     }
437
438     return location;
439   }
440
441   @Override
442   public void setLocation(@NotNull RelativePoint point) {
443     if (isRealPopup()) {
444       myPopup.setLocation(point.getScreenPoint());
445     }
446     else {
447       if (myCurrentIdeTooltip != null) {
448         Point screenPoint = point.getScreenPoint();
449         if (!screenPoint.equals(new RelativePoint(myCurrentIdeTooltip.getComponent(), myCurrentIdeTooltip.getPoint()).getScreenPoint())) {
450           myCurrentIdeTooltip.setPoint(point.getPoint());
451           myCurrentIdeTooltip.setComponent(point.getComponent());
452           IdeTooltipManager.getInstance().show(myCurrentIdeTooltip, true, false);
453         }
454       }
455       else {
456         Point targetPoint = point.getPoint(myComponent.getParent());
457         myComponent.setLocation(targetPoint);
458
459         myComponent.revalidate();
460         myComponent.repaint();
461       }
462     }
463   }
464
465   public void setSize(final Dimension size) {
466     if (myIsRealPopup && myPopup != null) {
467       // There is a possible case that a popup wraps target content component into other components which might have borders.
468       // That's why we can't just apply component's size to the whole popup. It needs to be adjusted before that.
469       JComponent popupContent = myPopup.getContent();
470       int widthExpand = 0;
471       int heightExpand = 0;
472       boolean adjustSize = false;
473       JComponent prev = myComponent;
474       for (Container c = myComponent.getParent(); c != null; c = c.getParent()) {
475         if (c == popupContent) {
476           adjustSize = true;
477           break;
478         }
479         if (c instanceof JComponent) {
480           Border border = ((JComponent)c).getBorder();
481           if (prev != null && border != null) {
482             Insets insets = border.getBorderInsets(prev);
483             widthExpand += insets.left + insets.right;
484             heightExpand += insets.top + insets.bottom;
485           }
486           prev = (JComponent)c;
487         }
488         else {
489           prev = null;
490         }
491       }
492       Dimension sizeToUse = size;
493       if (adjustSize && (widthExpand != 0 || heightExpand != 0)) {
494         sizeToUse = new Dimension(size.width + widthExpand, size.height + heightExpand);
495       }
496       myPopup.setSize(sizeToUse);
497     }
498     else if (!isAwtTooltip()) {
499       myComponent.setSize(size);
500
501       myComponent.revalidate();
502       myComponent.repaint();
503     }
504   }
505
506   public boolean isAwtTooltip() {
507     return myHintHint != null && myHintHint.isAwtTooltip();
508   }
509
510   public Dimension getSize() {
511     return myComponent.getSize();
512   }
513
514   public boolean isInsideHint(RelativePoint target) {
515     if (myComponent == null || !myComponent.isShowing()) return false;
516
517     if (myIsRealPopup) {
518       Window wnd = SwingUtilities.getWindowAncestor(myComponent);
519       return wnd.getBounds().contains(target.getScreenPoint());
520     }
521     else if (myCurrentIdeTooltip != null) {
522       return myCurrentIdeTooltip.isInside(target);
523     }
524     else {
525       return new Rectangle(myComponent.getLocationOnScreen(), myComponent.getSize()).contains(target.getScreenPoint());
526     }
527   }
528
529   private final class MyEscListener implements ActionListener {
530     @Override
531     public final void actionPerformed(final ActionEvent e) {
532       hide();
533     }
534   }
535
536   @Override
537   public String toString() {
538     return getComponent().toString();
539   }
540
541   public boolean canControlAutoHide() {
542     return myCurrentIdeTooltip != null && myCurrentIdeTooltip.getTipComponent().isShowing();
543   }
544
545   public IdeTooltip getCurrentIdeTooltip() {
546     return myCurrentIdeTooltip;
547   }
548 }