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