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