[dbe] data sources ui cleanup
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / BalloonImpl.java
1 /*
2  * Copyright 2000-2016 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.icons.AllIcons;
19 import com.intellij.ide.IdeEventQueue;
20 import com.intellij.ide.IdeTooltip;
21 import com.intellij.ide.RemoteDesktopDetector;
22 import com.intellij.openapi.MnemonicHelper;
23 import com.intellij.openapi.actionSystem.ActionManager;
24 import com.intellij.openapi.actionSystem.AnAction;
25 import com.intellij.openapi.actionSystem.AnActionEvent;
26 import com.intellij.openapi.actionSystem.DataContext;
27 import com.intellij.openapi.actionSystem.ex.AnActionListener;
28 import com.intellij.openapi.actionSystem.impl.ActionMenu;
29 import com.intellij.openapi.application.ApplicationManager;
30 import com.intellij.openapi.diagnostic.Logger;
31 import com.intellij.openapi.ui.GraphicsConfig;
32 import com.intellij.openapi.ui.impl.ShadowBorderPainter;
33 import com.intellij.openapi.ui.popup.Balloon;
34 import com.intellij.openapi.ui.popup.JBPopupListener;
35 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
36 import com.intellij.openapi.util.*;
37 import com.intellij.openapi.util.registry.Registry;
38 import com.intellij.openapi.wm.FocusRequestor;
39 import com.intellij.openapi.wm.IdeFocusManager;
40 import com.intellij.openapi.wm.IdeGlassPaneUtil;
41 import com.intellij.openapi.wm.impl.IdeGlassPaneEx;
42 import com.intellij.ui.awt.RelativePoint;
43 import com.intellij.ui.components.panels.NonOpaquePanel;
44 import com.intellij.ui.components.panels.Wrapper;
45 import com.intellij.util.Alarm;
46 import com.intellij.util.Consumer;
47 import com.intellij.util.IJSwingUtilities;
48 import com.intellij.util.containers.HashSet;
49 import com.intellij.util.ui.*;
50 import org.intellij.lang.annotations.JdkConstants;
51 import org.jetbrains.annotations.NotNull;
52 import org.jetbrains.annotations.Nullable;
53
54 import javax.swing.*;
55 import javax.swing.border.EmptyBorder;
56 import javax.swing.event.HyperlinkEvent;
57 import java.awt.*;
58 import java.awt.event.*;
59 import java.awt.geom.Area;
60 import java.awt.geom.GeneralPath;
61 import java.awt.geom.Rectangle2D;
62 import java.awt.geom.RoundRectangle2D;
63 import java.awt.image.BufferedImage;
64 import java.awt.image.ImageFilter;
65 import java.awt.image.RGBImageFilter;
66 import java.util.Collections;
67 import java.util.List;
68 import java.util.Set;
69 import java.util.concurrent.CopyOnWriteArraySet;
70
71 public class BalloonImpl implements Balloon, IdeTooltip.Ui {
72   public static final int DIALOG_ARC = 6;
73   public static final int ARC = 3;
74   public static final int DIALOG_TOPBOTTOM_POINTER_WIDTH = 24;
75   public static final int DIALOG_POINTER_WIDTH = 17;
76   public static final int TOPBOTTOM_POINTER_WIDTH = 14;
77   public static final int POINTER_WIDTH = 11;
78   public static final int DIALOG_TOPBOTTOM_POINTER_LENGTH = 16;
79   public static final int DIALOG_POINTER_LENGTH = 14;
80   public static final int TOPBOTTOM_POINTER_LENGTH = 10;
81   public static final int POINTER_LENGTH = 8;
82
83   private final Alarm myFadeoutAlarm = new Alarm(this);
84   private long myFadeoutRequestMillis = 0;
85   private int myFadeoutRequestDelay = 0;
86
87   private MyComponent myComp;
88   private JLayeredPane myLayeredPane;
89   private AbstractPosition myPosition;
90   private Point myTargetPoint;
91   private final boolean myHideOnFrameResize;
92   private final boolean myHideOnLinkClick;
93
94   private final Color myBorderColor;
95   private final Insets myBorderInsets;
96   private final Color myFillColor;
97
98   private final Insets myContainerInsets;
99
100   private boolean myLastMoveWasInsideBalloon;
101
102   private Rectangle myForcedBounds;
103
104   private ActionProvider myActionProvider;
105   private List<ActionButton> myActionButtons;
106
107   private final AWTEventListener myAwtActivityListener = new AWTEventListener() {
108     @Override
109     public void eventDispatched(final AWTEvent e) {
110       final int id = e.getID();
111       if (e instanceof MouseEvent) {
112         final MouseEvent me = (MouseEvent)e;
113         final boolean insideBalloon = isInsideBalloon(me);
114
115         if (myHideOnMouse && id == MouseEvent.MOUSE_PRESSED) {
116           if (!insideBalloon && !isWithinChildWindow(me)) {
117             if (myHideListener == null) {
118               hide();
119             }
120             else {
121               myHideListener.run();
122             }
123           }
124           return;
125         }
126
127         if (myClickHandler != null && id == MouseEvent.MOUSE_CLICKED) {
128           if (!(me.getComponent() instanceof CloseButton) && insideBalloon) {
129             myClickHandler.actionPerformed(new ActionEvent(me, ActionEvent.ACTION_PERFORMED, "click", me.getModifiersEx()));
130             if (myCloseOnClick) {
131               hide();
132               return;
133             }
134           }
135         }
136
137         if (myEnableButtons && id == MouseEvent.MOUSE_MOVED) {
138           final boolean moveChanged = insideBalloon != myLastMoveWasInsideBalloon;
139           myLastMoveWasInsideBalloon = insideBalloon;
140           if (moveChanged) {
141             if (insideBalloon && myFadeoutAlarm.getActiveRequestCount() > 0) { //Pause hiding timer when mouse is hover
142               myFadeoutAlarm.cancelAllRequests();
143               myFadeoutRequestDelay -= System.currentTimeMillis() - myFadeoutRequestMillis;
144             }
145             if (!insideBalloon && myFadeoutRequestDelay > 0) {
146               startFadeoutTimer(myFadeoutRequestDelay);
147             }
148             myComp.repaintButton();
149           }
150         }
151
152         if (UIUtil.isCloseClick(me)) {
153           if (isInsideBalloon(me)) {
154             hide();
155             me.consume();
156           }
157           return;
158         }
159       }
160
161       if ((myHideOnKey || myHideListener != null) && e instanceof KeyEvent && id == KeyEvent.KEY_PRESSED) {
162         final KeyEvent ke = (KeyEvent)e;
163         if (myHideListener != null) {
164           if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
165             myHideListener.run();
166           }
167           return;
168         }
169         if (ke.getKeyCode() != KeyEvent.VK_SHIFT &&
170             ke.getKeyCode() != KeyEvent.VK_CONTROL &&
171             ke.getKeyCode() != KeyEvent.VK_ALT &&
172             ke.getKeyCode() != KeyEvent.VK_META) {
173           if (SwingUtilities.isDescendingFrom(ke.getComponent(), myComp) || ke.getComponent() == myComp) return;
174           hide();
175         }
176       }
177     }
178   };
179
180   private boolean isWithinChildWindow(MouseEvent event) {
181     Component owner = UIUtil.getWindow(myContent);
182     if (owner != null) {
183       Component child = UIUtil.getWindow(event.getComponent());
184       if (child != owner) {
185         for (; child != null; child = child.getParent()) {
186           if (child == owner) {
187             return true;
188           }
189         }
190       }
191     }
192     return false;
193   }
194
195   private final long myFadeoutTime;
196   private Dimension myDefaultPrefSize;
197   private final ActionListener myClickHandler;
198   private final boolean myCloseOnClick;
199   private int myShadowSize = Registry.intValue("ide.balloon.shadow.size");
200   private ShadowBorderProvider myShadowBorderProvider;
201
202   private final CopyOnWriteArraySet<JBPopupListener> myListeners = new CopyOnWriteArraySet<JBPopupListener>();
203   private boolean myVisible;
204   private PositionTracker<Balloon> myTracker;
205   private final int myAnimationCycle;
206
207   private boolean myFadedIn;
208   private boolean myFadedOut;
209   private final int myCalloutShift;
210
211   private final int myPositionChangeXShift;
212   private final int myPositionChangeYShift;
213   private boolean myDialogMode;
214   private IdeFocusManager myFocusManager;
215   private final String myTitle;
216   private JLabel myTitleLabel;
217
218   private boolean myAnimationEnabled = true;
219   private boolean myShadow = UIUtil.isUnderDarcula();
220   private final Layer myLayer;
221   private final boolean myBlockClicks;
222   private RelativePoint myPrevMousePoint = null;
223
224   public boolean isInsideBalloon(MouseEvent me) {
225     return isInside(new RelativePoint(me));
226   }
227
228   @Override
229   public boolean isInside(@NotNull RelativePoint target) {
230     if (myComp == null) return false;
231     Component cmp = target.getOriginalComponent();
232
233     if (!cmp.isShowing()) return true;
234     if (cmp instanceof MenuElement) return false;
235     if (myActionButtons != null) {
236       for (ActionButton button : myActionButtons) {
237         if (cmp == button) return true;
238       }
239     }
240     if (UIUtil.isDescendingFrom(cmp, myComp)) return true;
241     if (myComp == null || !myComp.isShowing()) return false;
242     Point point = target.getScreenPoint();
243     SwingUtilities.convertPointFromScreen(point, myComp);
244     return myComp.contains(point);
245   }
246
247   public boolean isMovingForward(RelativePoint target) {
248     try {
249       if (myComp == null || !myComp.isShowing()) return false;
250       if (myPrevMousePoint == null) return true;
251       if (myPrevMousePoint.getComponent() != target.getComponent()) return false;
252       Rectangle rectangleOnScreen = new Rectangle(myComp.getLocationOnScreen(), myComp.getSize());
253       return ScreenUtil.isMovementTowards(myPrevMousePoint.getScreenPoint(), target.getScreenPoint(), rectangleOnScreen);
254     }
255     finally {
256       myPrevMousePoint = target;
257     }
258   }
259
260   private final ComponentAdapter myComponentListener = new ComponentAdapter() {
261     @Override
262     public void componentResized(final ComponentEvent e) {
263       if (myHideOnFrameResize) {
264         hide();
265       }
266     }
267   };
268   private Animator myAnimator;
269   private boolean myShowPointer;
270
271   private boolean myDisposed;
272   private final JComponent myContent;
273   private boolean myHideOnMouse;
274   private Runnable myHideListener;
275   private final boolean myHideOnKey;
276   private final boolean myHideOnAction;
277   private final boolean myRequestFocus;
278   private Component myOriginalFocusOwner;
279   private final boolean myEnableButtons;
280
281   public BalloonImpl(@NotNull JComponent content,
282                      @NotNull Color borderColor,
283                      Insets borderInsets,
284                      @NotNull Color fillColor,
285                      boolean hideOnMouse,
286                      boolean hideOnKey,
287                      boolean hideOnAction,
288                      boolean showPointer,
289                      boolean enableButtons,
290                      long fadeoutTime,
291                      boolean hideOnFrameResize,
292                      boolean hideOnLinkClick,
293                      ActionListener clickHandler,
294                      boolean closeOnClick,
295                      int animationCycle,
296                      int calloutShift,
297                      int positionChangeXShift,
298                      int positionChangeYShift,
299                      boolean dialogMode,
300                      String title,
301                      Insets contentInsets,
302                      boolean shadow,
303                      boolean smallVariant,
304                      boolean blockClicks,
305                      Layer layer,
306                      boolean requestFocus) {
307     myBorderColor = borderColor;
308     myBorderInsets = borderInsets != null ? borderInsets : new Insets(3, 3, 3, 3);
309     myFillColor = fillColor;
310     myContent = content;
311     myHideOnMouse = hideOnMouse;
312     myHideOnKey = hideOnKey;
313     myHideOnAction = hideOnAction;
314     myShowPointer = showPointer;
315     myEnableButtons = enableButtons;
316     myHideOnFrameResize = hideOnFrameResize;
317     myHideOnLinkClick = hideOnLinkClick;
318     myClickHandler = clickHandler;
319     myCloseOnClick = closeOnClick;
320     myCalloutShift = calloutShift;
321     myPositionChangeXShift = positionChangeXShift;
322     myPositionChangeYShift = positionChangeYShift;
323     myDialogMode = dialogMode;
324     myTitle = title;
325     myLayer = layer != null ? layer : Layer.normal;
326     myBlockClicks = blockClicks;
327     myRequestFocus = requestFocus;
328     MnemonicHelper.init(content);
329
330     if (!myDialogMode) {
331       new AwtVisitor(content) {
332         @Override
333         public boolean visit(Component component) {
334           if (component instanceof JLabel) {
335             JLabel label = (JLabel)component;
336             if (label.getDisplayedMnemonic() != '\0' || label.getDisplayedMnemonicIndex() >= 0) {
337               myDialogMode = true;
338               return true;
339             }
340           }
341           else if (component instanceof JCheckBox) {
342             JCheckBox checkBox = (JCheckBox)component;
343             if (checkBox.getMnemonic() >= 0 || checkBox.getDisplayedMnemonicIndex() >= 0) {
344               myDialogMode = true;
345               return true;
346             }
347           }
348           return false;
349         }
350       };
351     }
352
353     myShadow = shadow;
354     myShadowSize = Registry.intValue("ide.balloon.shadow.size");
355     myContainerInsets = contentInsets;
356
357     myFadeoutTime = fadeoutTime;
358     myAnimationCycle = animationCycle;
359
360     if (smallVariant) {
361       new AwtVisitor(myContent) {
362         @Override
363         public boolean visit(Component component) {
364           UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, component);
365           return false;
366         }
367       };
368     }
369   }
370
371   @Override
372   public void show(final RelativePoint target, final Balloon.Position position) {
373     show(target, getAbstractPositionFor(position));
374   }
375
376   public int getLayer() {
377     Integer result = JLayeredPane.DEFAULT_LAYER;
378     switch (myLayer) {
379       case normal:
380         result = JLayeredPane.POPUP_LAYER;
381         break;
382       case top:
383         result = JLayeredPane.DRAG_LAYER;
384         break;
385     }
386
387     return result;
388   }
389
390   private static AbstractPosition getAbstractPositionFor(Position position) {
391     switch (position) {
392       case atLeft:
393         return AT_LEFT;
394       case atRight:
395         return AT_RIGHT;
396       case below:
397         return BELOW;
398       case above:
399         return ABOVE;
400       default:
401         return BELOW;
402     }
403   }
404
405   @Override
406   public void show(PositionTracker<Balloon> tracker, Balloon.Position position) {
407     show(tracker, getAbstractPositionFor(position));
408   }
409
410   private Insets getInsetsCopy() {
411     return new Insets(myBorderInsets.top, myBorderInsets.left, myBorderInsets.bottom, myBorderInsets.right);
412   }
413
414   private void show(RelativePoint target, AbstractPosition position) {
415     show(new PositionTracker.Static<Balloon>(target), position);
416   }
417
418   private void show(PositionTracker<Balloon> tracker, AbstractPosition position) {
419     assert !myDisposed : "Balloon is already disposed";
420
421     if (isVisible()) return;
422     final Component comp = tracker.getComponent();
423     if (!comp.isShowing()) return;
424
425     myTracker = tracker;
426     myTracker.init(this);
427
428     JRootPane root = null;
429     JDialog dialog = IJSwingUtilities.findParentOfType(comp, JDialog.class);
430     if (dialog != null) {
431       root = dialog.getRootPane();
432     }
433     else {
434       JWindow jwindow = IJSwingUtilities.findParentOfType(comp, JWindow.class);
435       if (jwindow != null) {
436         root = jwindow.getRootPane();
437       }
438       else {
439         JFrame frame = IJSwingUtilities.findParentOfType(comp, JFrame.class);
440         if (frame != null) {
441           root = frame.getRootPane();
442         }
443         else {
444           assert false;
445         }
446       }
447     }
448
449     myVisible = true;
450
451     myLayeredPane = root.getLayeredPane();
452     myPosition = position;
453     UIUtil.setFutureRootPane(myContent, root);
454
455     myFocusManager = IdeFocusManager.findInstanceByComponent(myLayeredPane);
456     final Ref<Component> originalFocusOwner = new Ref<Component>();
457     final Ref<FocusRequestor> focusRequestor = new Ref<FocusRequestor>();
458     final Ref<ActionCallback> proxyFocusRequest = new Ref<ActionCallback>(ActionCallback.DONE);
459
460     boolean mnemonicsFix = myDialogMode && SystemInfo.isMac && Registry.is("ide.mac.inplaceDialogMnemonicsFix");
461     if (mnemonicsFix) {
462       final IdeGlassPaneEx glassPane = (IdeGlassPaneEx)IdeGlassPaneUtil.find(myLayeredPane);
463       assert glassPane != null;
464
465       proxyFocusRequest.set(new ActionCallback());
466
467       myFocusManager.doWhenFocusSettlesDown(new ExpirableRunnable() {
468         @Override
469         public boolean isExpired() {
470           return isDisposed();
471         }
472
473         @Override
474         public void run() {
475           IdeEventQueue.getInstance().disableInputMethods(BalloonImpl.this);
476           originalFocusOwner.set(myFocusManager.getFocusOwner());
477           focusRequestor.set(myFocusManager.getFurtherRequestor());
478         }
479       });
480     }
481     if (myRequestFocus) {
482       myFocusManager.doWhenFocusSettlesDown(new ExpirableRunnable() {
483         @Override
484         public boolean isExpired() {
485           return isDisposed();
486         }
487
488         @Override
489         public void run() {
490           myOriginalFocusOwner = myFocusManager.getFocusOwner();
491
492           // Set the focus to "myContent"
493           myFocusManager.requestFocus(getContentToFocus(), true);
494         }
495       });
496     }
497
498     myLayeredPane.addComponentListener(myComponentListener);
499
500     myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutShift);
501
502     int positionChangeFix = 0;
503     if (myShowPointer) {
504       Rectangle rec = getRecForPosition(myPosition, true);
505
506       if (!myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc())) {
507         rec = getRecForPosition(myPosition, false);
508
509         Rectangle lp = new Rectangle(new Point(myContainerInsets.left, myContainerInsets.top), myLayeredPane.getSize());
510         lp.width -= myContainerInsets.right;
511         lp.height -= myContainerInsets.bottom;
512
513         if (!lp.contains(rec)) {
514           Rectangle2D currentSquare = lp.createIntersection(rec);
515
516           double maxSquare = currentSquare.getWidth() * currentSquare.getHeight();
517           AbstractPosition targetPosition = myPosition;
518
519           for (AbstractPosition eachPosition : myPosition.getOtherPositions()) {
520             Rectangle2D eachIntersection = lp.createIntersection(getRecForPosition(eachPosition, false));
521             double eachSquare = eachIntersection.getWidth() * eachIntersection.getHeight();
522             if (maxSquare < eachSquare) {
523               maxSquare = eachSquare;
524               targetPosition = eachPosition;
525             }
526           }
527
528           myPosition = targetPosition;
529           positionChangeFix = myPosition.getChangeShift(position, myPositionChangeXShift, myPositionChangeYShift);
530         }
531       }
532     }
533
534     if (myPosition != position) {
535       myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane),
536                                                  myCalloutShift > 0 ? myCalloutShift + positionChangeFix : positionChangeFix);
537     }
538
539     createComponent();
540
541     myComp.validate();
542
543     Rectangle rec = myComp.getContentBounds();
544
545     if (myShowPointer &&
546         !myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc())) {
547       myShowPointer = false;
548       myComp.removeAll();
549       myLayeredPane.remove(myComp);
550
551       createComponent();
552       if (!new Rectangle(myLayeredPane.getSize())
553         .contains(new Rectangle(myComp.getSize()))) { // Balloon is bigger than window, don't show it at all.
554         myComp.removeAll();
555         myLayeredPane.remove(myComp);
556         myLayeredPane = null;
557         hide();
558         return;
559       }
560     }
561
562     for (JBPopupListener each : myListeners) {
563       each.beforeShown(new LightweightWindowEvent(this));
564     }
565
566     runAnimation(true, myLayeredPane, null);
567
568     myLayeredPane.revalidate();
569     myLayeredPane.repaint();
570
571     if (mnemonicsFix) {
572       proxyFocusRequest.get().doWhenDone(new Runnable() {
573         @Override
574         public void run() {
575           myFocusManager.requestFocus(originalFocusOwner.get(), true);
576         }
577       });
578     }
579
580     Toolkit.getDefaultToolkit().addAWTEventListener(
581       myAwtActivityListener, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);
582
583     if (ApplicationManager.getApplication() != null) {
584       ActionManager.getInstance().addAnActionListener(new AnActionListener.Adapter() {
585         @Override
586         public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) {
587           if (myHideOnAction) {
588             hide();
589           }
590         }
591       }, this);
592     }
593
594     if (myHideOnLinkClick) {
595       final Ref<JEditorPane> ref = Ref.create(null);
596       new AwtVisitor(myContent) {
597         @Override
598         public boolean visit(Component component) {
599           if (component instanceof JEditorPane) {
600             ref.set((JEditorPane)component);
601             return true;
602           }
603           return false;
604         }
605       };
606       if (!ref.isNull()) {
607         ref.get().addHyperlinkListener(new HyperlinkAdapter() {
608           @Override
609           protected void hyperlinkActivated(HyperlinkEvent e) {
610             hide();
611           }
612         });
613       }
614     }
615   }
616
617   /**
618    * Figure out the component to focus inside the {@link myContent} field.
619    */
620   @NotNull
621   private Component getContentToFocus() {
622     Component focusComponent = myContent;
623     while (true) {
624       // Setting focus to a JScrollPane is not very useful. Better setting focus to the
625       // contained view. This is useful for Tooltip popups, for example.
626       if (focusComponent instanceof JScrollPane) {
627         JViewport viewport = ((JScrollPane)focusComponent).getViewport();
628         if (viewport == null) {
629           break;
630         }
631         Component child = viewport.getView();
632         if (child == null) {
633           break;
634         }
635         focusComponent = child;
636         continue;
637       }
638
639       // Done if we can't find anything to dive into
640       break;
641     }
642     return focusComponent;
643   }
644
645   private Rectangle getRecForPosition(AbstractPosition position, boolean adjust) {
646     Dimension size = getContentSizeFor(position);
647     Rectangle rec = new Rectangle(new Point(0, 0), size);
648
649     position.setRecToRelativePosition(rec, myTargetPoint);
650
651     if (adjust) {
652       rec = myPosition.getUpdatedBounds(myLayeredPane.getSize(), myForcedBounds, rec.getSize(), myShowPointer, myTargetPoint,
653                                         myContainerInsets);
654     }
655
656     return rec;
657   }
658
659   private Dimension getContentSizeFor(AbstractPosition position) {
660     Dimension size = myContent.getPreferredSize();
661     if (myShadowBorderProvider == null) {
662       JBInsets.addTo(size, position.createBorder(this).getBorderInsets());
663     }
664     return size;
665   }
666
667   private void disposeButton(ActionButton button) {
668     if (button != null && button.getParent() != null) {
669       Container parent = button.getParent();
670       parent.remove(button);
671       //noinspection RedundantCast
672       ((JComponent)parent).revalidate();
673       parent.repaint();
674     }
675   }
676
677   public JComponent getContent() {
678     return myContent;
679   }
680
681   public Component getComponent() {
682     return myComp;
683   }
684
685   private void createComponent() {
686     myComp = new MyComponent(myContent, this, myShadowBorderProvider != null ? null :
687                                               myShowPointer ? myPosition.createBorder(this) : getPointlessBorder());
688
689     if (myActionProvider == null) {
690       final Consumer<MouseEvent> listener = new Consumer<MouseEvent>() {
691         @Override
692         public void consume(MouseEvent event) {
693           //noinspection SSBasedInspection
694           SwingUtilities.invokeLater(new Runnable() {
695             @Override
696             public void run() {
697               hide();
698             }
699           });
700         }
701       };
702
703       myActionProvider = new ActionProvider() {
704         private ActionButton myCloseButton;
705
706         @NotNull
707         @Override
708         public List<ActionButton> createActions() {
709           myCloseButton = new CloseButton(listener);
710           return Collections.singletonList(myCloseButton);
711         }
712
713         @Override
714         public void layout(@NotNull Rectangle lpBounds) {
715           if (!myCloseButton.isVisible()) {
716             return;
717           }
718
719           Icon icon = getCloseButton();
720           int iconWidth = icon.getIconWidth();
721           int iconHeight = icon.getIconHeight();
722           Rectangle r =
723             new Rectangle(lpBounds.x + lpBounds.width - iconWidth + (int)(iconWidth * 0.3), lpBounds.y - (int)(iconHeight * 0.3), iconWidth,
724                           iconHeight);
725
726           Insets border = getShadowBorderInsets();
727           r.x -= border.left;
728           r.y -= border.top;
729
730           myCloseButton.setBounds(r);
731         }
732       };
733     }
734
735     myComp.clear();
736     myComp.myAlpha = isAnimationEnabled() ? 0f : -1;
737
738     myComp.setBorder(new EmptyBorder(getShadowBorderInsets()));
739
740     myLayeredPane.add(myComp);
741     myLayeredPane.setLayer(myComp, getLayer(), 0); // the second balloon must be over the first one
742     myPosition.updateBounds(this);
743     if (myBlockClicks) {
744       myComp.addMouseListener(new MouseAdapter() {
745         @Override
746         public void mouseClicked(MouseEvent e) {
747           e.consume();
748         }
749
750         @Override
751         public void mousePressed(MouseEvent e) {
752           e.consume();
753         }
754
755         @Override
756         public void mouseReleased(MouseEvent e) {
757           e.consume();
758         }
759       });
760     }
761   }
762
763
764   @NotNull
765   private EmptyBorder getPointlessBorder() {
766     return new EmptyBorder(myBorderInsets);
767   }
768
769   @Override
770   public void revalidate() {
771     revalidate(myTracker);
772   }
773
774   @Override
775   public void revalidate(@NotNull PositionTracker<Balloon> tracker) {
776     RelativePoint newPosition = tracker.recalculateLocation(this);
777
778     if (newPosition != null) {
779       myTargetPoint = myPosition.getShiftedPoint(newPosition.getPoint(myLayeredPane), myCalloutShift);
780       myPosition.updateBounds(this);
781     }
782   }
783
784   public void setShadowBorderProvider(@NotNull ShadowBorderProvider provider) {
785     myShadowBorderProvider = provider;
786   }
787
788   public int getShadowBorderSize() {
789     return hasShadow() ? myShadowSize : 0;
790   }
791
792   @NotNull
793   public Insets getShadowBorderInsets() {
794     if (myShadowBorderProvider != null) {
795       return myShadowBorderProvider.getInsets();
796     }
797     int size = getShadowBorderSize();
798     return new Insets(size, size, size, size);
799   }
800
801   public boolean hasShadow() {
802     return myShadowBorderProvider != null || (myShadow && Registry.is("ide.balloon.shadowEnabled"));
803   }
804
805   public interface ShadowBorderProvider {
806     @NotNull
807     Insets getInsets();
808
809     void paintShadow(@NotNull JComponent component, @NotNull Graphics g);
810
811     void paintBorder(@NotNull Rectangle bounds, @NotNull Graphics2D g);
812
813     void paintPointingShape(@NotNull Rectangle bounds, @NotNull Point pointTarget, @NotNull Position position, @NotNull Graphics2D g);
814   }
815
816   @Override
817   public void show(JLayeredPane pane) {
818     show(pane, null);
819   }
820
821   @Override
822   public void showInCenterOf(JComponent component) {
823     final Dimension size = component.getSize();
824     show(new RelativePoint(component, new Point(size.width / 2, size.height / 2)), Balloon.Position.above);
825   }
826
827   public void show(JLayeredPane pane, @Nullable Rectangle bounds) {
828     if (bounds != null) {
829       myForcedBounds = bounds;
830     }
831     show(new RelativePoint(pane, new Point(0, 0)), Balloon.Position.above);
832   }
833
834
835   private void runAnimation(boolean forward, final JLayeredPane layeredPane, @Nullable final Runnable onDone) {
836     if (myAnimator != null) {
837       Disposer.dispose(myAnimator);
838     }
839
840     myAnimator = new Animator("Balloon", 8, isAnimationEnabled() ? myAnimationCycle : 0, false, forward) {
841       @Override
842       public void paintNow(final int frame, final int totalFrames, final int cycle) {
843         if (myComp == null || myComp.getParent() == null || !isAnimationEnabled()) return;
844         myComp.setAlpha((float)frame / totalFrames);
845       }
846
847       @Override
848       protected void paintCycleEnd() {
849         if (myComp == null || myComp.getParent() == null) return;
850
851         if (isForward()) {
852           myComp.clear();
853           myComp.repaint();
854
855           myFadedIn = true;
856
857           startFadeoutTimer((int)myFadeoutTime);
858         }
859         else {
860           layeredPane.remove(myComp);
861           layeredPane.revalidate();
862           layeredPane.repaint();
863         }
864         Disposer.dispose(this);
865       }
866
867       @Override
868       public void dispose() {
869         super.dispose();
870         myAnimator = null;
871         if (onDone != null) {
872           onDone.run();
873         }
874       }
875     };
876
877     myAnimator.resume();
878   }
879
880   public void startFadeoutTimer(final int fadeoutDelay) {
881     if (fadeoutDelay > 0) {
882       myFadeoutAlarm.cancelAllRequests();
883       myFadeoutRequestMillis = System.currentTimeMillis();
884       myFadeoutRequestDelay = fadeoutDelay;
885       myFadeoutAlarm.addRequest(new Runnable() {
886         @Override
887         public void run() {
888           hide();
889         }
890       }, fadeoutDelay, null);
891     }
892   }
893
894
895   int getArc() {
896     return myDialogMode ? DIALOG_ARC : ARC;
897   }
898
899   int getPointerWidth(AbstractPosition position) {
900     if (myDialogMode) {
901       return position.isTopBottomPointer() ? DIALOG_TOPBOTTOM_POINTER_WIDTH : DIALOG_POINTER_WIDTH;
902     }
903     else {
904       return position.isTopBottomPointer() ? TOPBOTTOM_POINTER_WIDTH : POINTER_WIDTH;
905     }
906   }
907
908   public static int getNormalInset() {
909     return 3;
910   }
911
912   int getPointerLength(AbstractPosition position) {
913     return getPointerLength(position, myDialogMode);
914   }
915
916   static int getPointerLength(AbstractPosition position, boolean dialogMode) {
917     if (dialogMode) {
918       return position.isTopBottomPointer() ? DIALOG_TOPBOTTOM_POINTER_LENGTH : DIALOG_POINTER_LENGTH;
919     }
920     else {
921       return position.isTopBottomPointer() ? TOPBOTTOM_POINTER_LENGTH : POINTER_LENGTH;
922     }
923   }
924
925   public static int getPointerLength(Position position, boolean dialogMode) {
926     return getPointerLength(getAbstractPositionFor(position), dialogMode);
927   }
928
929   @Override
930   public void hide() {
931     hide(false);
932   }
933
934   @Override
935   public void hide(boolean ok) {
936     hideAndDispose(ok);
937   }
938
939   @Override
940   public void dispose() {
941     hideAndDispose(false);
942   }
943
944   private boolean myTraceDispose;
945
946   public void traceDispose(boolean value) {
947     myTraceDispose = value;
948   }
949
950   private void hideAndDispose(final boolean ok) {
951     if (myDisposed) return;
952
953     if (myTraceDispose) {
954       Logger.getInstance("#com.intellij.ui.BalloonImpl").error("Dispose balloon before showing", new Throwable());
955     }
956
957     myDisposed = true;
958     hideComboBoxPopups();
959
960     final Runnable disposeRunnable = new Runnable() {
961       @Override
962       public void run() {
963         myFadedOut = true;
964         if (myRequestFocus) {
965           if (myOriginalFocusOwner != null) {
966             myFocusManager.requestFocus(myOriginalFocusOwner, false);
967           }
968         }
969
970         for (JBPopupListener each : myListeners) {
971           each.onClosed(new LightweightWindowEvent(BalloonImpl.this, ok));
972         }
973
974         Disposer.dispose(BalloonImpl.this);
975         onDisposed();
976       }
977     };
978
979     Toolkit.getDefaultToolkit().removeAWTEventListener(myAwtActivityListener);
980     if (myLayeredPane != null) {
981       myLayeredPane.removeComponentListener(myComponentListener);
982
983       runAnimation(false, myLayeredPane, disposeRunnable);
984     }
985     else {
986       disposeRunnable.run();
987     }
988
989     myVisible = false;
990     myTracker = null;
991   }
992
993   private void hideComboBoxPopups() {
994     List<JComboBox> comboBoxes = UIUtil.findComponentsOfType(myComp, JComboBox.class);
995     for (JComboBox box : comboBoxes) {
996       box.hidePopup();
997     }
998   }
999
1000   protected void onDisposed() {
1001   }
1002
1003   @Override
1004   public void addListener(@NotNull JBPopupListener listener) {
1005     myListeners.add(listener);
1006   }
1007
1008   public boolean isVisible() {
1009     return myVisible;
1010   }
1011
1012   public void setHideOnClickOutside(boolean hideOnMouse) {
1013     myHideOnMouse = hideOnMouse;
1014   }
1015
1016   public void setHideListener(@NotNull Runnable listener) {
1017     myHideListener = listener;
1018     myHideOnMouse = true;
1019   }
1020
1021   public void setShowPointer(final boolean show) {
1022     myShowPointer = show;
1023   }
1024
1025   @SuppressWarnings("MethodMayBeStatic")
1026   public Icon getCloseButton() {
1027     return AllIcons.General.BalloonClose;
1028   }
1029
1030   @Override
1031   public void setBounds(Rectangle bounds) {
1032     myForcedBounds = bounds;
1033     if (myPosition != null) {
1034       myPosition.updateBounds(this);
1035     }
1036   }
1037
1038   public void setShadowSize(int shadowSize) {
1039     myShadowSize = shadowSize;
1040   }
1041
1042   @Override
1043   public Dimension getPreferredSize() {
1044     if (myComp != null) {
1045       return myComp.getPreferredSize();
1046     }
1047     if (myDefaultPrefSize == null) {
1048       final EmptyBorder border = myShadowBorderProvider == null ? getPointlessBorder() : null;
1049       final MyComponent c = new MyComponent(myContent, this, border);
1050       if (myShadowBorderProvider != null) {
1051         c.setBorder(new EmptyBorder(getShadowBorderInsets()));
1052       }
1053       myDefaultPrefSize = c.getPreferredSize();
1054     }
1055     return myDefaultPrefSize;
1056   }
1057
1058   private abstract static class AbstractPosition {
1059     abstract EmptyBorder createBorder(final BalloonImpl balloon);
1060
1061
1062     abstract void setRecToRelativePosition(Rectangle rec, Point targetPoint);
1063
1064     abstract int getChangeShift(AbstractPosition original, int xShift, int yShift);
1065
1066     public void updateBounds(final BalloonImpl balloon) {
1067       if (balloon.myLayeredPane == null || balloon.myComp == null) return;
1068
1069       final Rectangle bounds =
1070         getUpdatedBounds(balloon.myLayeredPane.getSize(), balloon.myForcedBounds, balloon.myComp.getPreferredSize(), balloon.myShowPointer,
1071                          balloon.myTargetPoint, balloon.myContainerInsets);
1072
1073       if (balloon.myShadowBorderProvider == null) {
1074         bounds.setLocation(getShiftedPoint(bounds.getLocation(), balloon.getShadowBorderInsets()));
1075       }
1076       balloon.myComp._setBounds(bounds);
1077     }
1078
1079     public Rectangle getUpdatedBounds(Dimension layeredPaneSize,
1080                                       Rectangle forcedBounds,
1081                                       Dimension preferredSize,
1082                                       boolean showPointer,
1083                                       Point point,
1084                                       Insets containerInsets) {
1085
1086       Rectangle bounds = forcedBounds;
1087
1088       if (bounds == null) {
1089         Point location = showPointer
1090                          ? getLocation(layeredPaneSize, point, preferredSize)
1091                          : new Point(point.x - preferredSize.width / 2, point.y - preferredSize.height / 2);
1092         bounds = new Rectangle(location.x, location.y, preferredSize.width, preferredSize.height);
1093
1094         ScreenUtil.moveToFit(bounds, new Rectangle(0, 0, layeredPaneSize.width, layeredPaneSize.height), containerInsets);
1095       }
1096
1097       return bounds;
1098     }
1099
1100     abstract Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize);
1101
1102     void paintComponent(BalloonImpl balloon, final Rectangle bounds, final Graphics2D g, Point pointTarget) {
1103       final GraphicsConfig cfg = new GraphicsConfig(g);
1104       cfg.setAntialiasing(true);
1105
1106       if (balloon.myShadowBorderProvider != null) {
1107         balloon.myShadowBorderProvider.paintBorder(bounds, g);
1108         if (balloon.myShowPointer) {
1109           Position position;
1110           if (this == ABOVE) {
1111             position = Position.above;
1112           }
1113           else if (this == BELOW) {
1114             position = Position.below;
1115           }
1116           else if (this == AT_LEFT) {
1117             position = Position.atLeft;
1118           }
1119           else {
1120             position = Position.atRight;
1121           }
1122           balloon.myShadowBorderProvider.paintPointingShape(bounds, pointTarget, position, g);
1123         }
1124         cfg.restore();
1125         return;
1126       }
1127
1128       Shape shape;
1129       if (balloon.myShowPointer) {
1130         shape = getPointingShape(bounds, pointTarget, balloon);
1131       }
1132       else {
1133         shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, balloon.getArc(), balloon.getArc());
1134       }
1135
1136       g.setColor(balloon.myFillColor);
1137       g.fill(shape);
1138       g.setColor(balloon.myBorderColor);
1139
1140       if (balloon.myTitleLabel != null) {
1141         Rectangle titleBounds = balloon.myTitleLabel.getBounds();
1142
1143         Insets inset = getTitleInsets(getNormalInset() - 1, balloon.getPointerLength(this) + 50);
1144         Insets borderInsets = balloon.getShadowBorderInsets();
1145         inset.top += borderInsets.top;
1146         inset.bottom += borderInsets.bottom;
1147         inset.left += borderInsets.left;
1148         inset.right += borderInsets.right;
1149
1150         titleBounds.x -= inset.left + 1;
1151         titleBounds.width += inset.left + inset.right + 50;
1152         titleBounds.y -= inset.top + 1;
1153         titleBounds.height += inset.top + inset.bottom + 1;
1154
1155         Area area = new Area(shape);
1156         area.intersect(new Area(titleBounds));
1157
1158
1159         Color fgColor = UIManager.getColor("Label.foreground");
1160         fgColor = ColorUtil.toAlpha(fgColor, 140);
1161         g.setColor(fgColor);
1162         g.fill(area);
1163
1164         g.setColor(balloon.myBorderColor);
1165         g.draw(area);
1166       }
1167
1168       g.draw(shape);
1169       cfg.restore();
1170     }
1171
1172     protected abstract Insets getTitleInsets(int normalInset, int pointerLength);
1173
1174     protected abstract Shape getPointingShape(final Rectangle bounds,
1175                                               final Point pointTarget,
1176                                               final BalloonImpl balloon);
1177
1178     public boolean isOkToHavePointer(Point targetPoint, Rectangle bounds, int pointerLength, int pointerWidth, int arc) {
1179       if (bounds.x < targetPoint.x &&
1180           bounds.x + bounds.width > targetPoint.x &&
1181           bounds.y < targetPoint.y &&
1182           bounds.y + bounds.height > targetPoint.y) {
1183         return false;
1184       }
1185
1186       Rectangle pointless = getPointlessContentRec(bounds, pointerLength);
1187
1188       int distance = getDistanceToTarget(pointless, targetPoint);
1189       if (distance < pointerLength - 1 || distance > 2 * pointerLength) return false;
1190
1191       UnfairTextRange balloonRange;
1192       UnfairTextRange pointerRange;
1193       if (isTopBottomPointer()) {
1194         balloonRange = new UnfairTextRange(bounds.x + arc - 1, bounds.x + bounds.width - arc * 2 + 1);
1195         pointerRange = new UnfairTextRange(targetPoint.x - pointerWidth / 2, targetPoint.x + pointerWidth / 2);
1196       }
1197       else {
1198         balloonRange = new UnfairTextRange(bounds.y + arc - 1, bounds.y + bounds.height - arc * 2 + 1);
1199         pointerRange = new UnfairTextRange(targetPoint.y - pointerWidth / 2, targetPoint.y + pointerWidth / 2);
1200       }
1201       return balloonRange.contains(pointerRange);
1202     }
1203
1204     protected abstract int getDistanceToTarget(Rectangle rectangle, Point targetPoint);
1205
1206     protected boolean isTopBottomPointer() {
1207       return this instanceof Below || this instanceof Above;
1208     }
1209
1210     protected abstract Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength);
1211
1212     public Set<AbstractPosition> getOtherPositions() {
1213       HashSet<AbstractPosition> all = new HashSet<AbstractPosition>();
1214       all.add(BELOW);
1215       all.add(ABOVE);
1216       all.add(AT_RIGHT);
1217       all.add(AT_LEFT);
1218
1219       all.remove(this);
1220
1221       return all;
1222     }
1223
1224     public abstract Point getShiftedPoint(Point targetPoint, int shift);
1225
1226     public abstract Point getShiftedPoint(Point targetPoint, Insets shift);
1227   }
1228
1229   public static final AbstractPosition BELOW = new Below();
1230   public static final AbstractPosition ABOVE = new Above();
1231   public static final AbstractPosition AT_RIGHT = new AtRight();
1232   public static final AbstractPosition AT_LEFT = new AtLeft();
1233
1234
1235   private static class Below extends AbstractPosition {
1236     @Override
1237     public Point getShiftedPoint(Point targetPoint, int shift) {
1238       return new Point(targetPoint.x, targetPoint.y + shift);
1239     }
1240
1241     @Override
1242     public Point getShiftedPoint(Point targetPoint, Insets shift) {
1243       return getShiftedPoint(targetPoint, -shift.top);
1244     }
1245
1246     @Override
1247     int getChangeShift(AbstractPosition original, int xShift, int yShift) {
1248       return original == ABOVE ? yShift : 0;
1249     }
1250
1251
1252     @Override
1253     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
1254       return rectangle.y - targetPoint.y;
1255     }
1256
1257     @Override
1258     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
1259       return new Rectangle(bounds.x, bounds.y + pointerLength, bounds.width, bounds.height - pointerLength);
1260     }
1261
1262     @Override
1263     EmptyBorder createBorder(final BalloonImpl balloon) {
1264       Insets insets = balloon.getInsetsCopy();
1265       insets.top += balloon.getPointerLength(this);
1266       return new EmptyBorder(insets);
1267     }
1268
1269     @Override
1270     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
1271       rec.setLocation(new Point(targetPoint.x - rec.width / 2, targetPoint.y));
1272     }
1273
1274     @Override
1275     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
1276       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
1277       return new Point(center.x, targetPoint.y);
1278     }
1279
1280     @Override
1281     protected Insets getTitleInsets(int normalInset, int pointerLength) {
1282       return new Insets(pointerLength, normalInset, normalInset, normalInset);
1283     }
1284
1285     @Override
1286     protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
1287       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.TOP);
1288       shaper.line(balloon.getPointerWidth(this) / 2, balloon.getPointerLength(this)).toRightCurve().roundRightDown().toBottomCurve()
1289         .roundLeftDown()
1290         .toLeftCurve().roundLeftUp().toTopCurve().roundUpRight()
1291         .lineTo(pointTarget.x - balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y);
1292       shaper.close();
1293
1294       return shaper.getShape();
1295     }
1296   }
1297
1298   private static class Above extends AbstractPosition {
1299     @Override
1300     public Point getShiftedPoint(Point targetPoint, int shift) {
1301       return new Point(targetPoint.x, targetPoint.y - shift);
1302     }
1303
1304     @Override
1305     public Point getShiftedPoint(Point targetPoint, Insets shift) {
1306       return getShiftedPoint(targetPoint, -shift.top);
1307     }
1308
1309     @Override
1310     int getChangeShift(AbstractPosition original, int xShift, int yShift) {
1311       return original == BELOW ? -yShift : 0;
1312     }
1313
1314     @Override
1315     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
1316       return targetPoint.y - (int)rectangle.getMaxY();
1317     }
1318
1319     @Override
1320     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
1321       return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height - pointerLength);
1322     }
1323
1324     @Override
1325     EmptyBorder createBorder(final BalloonImpl balloon) {
1326       Insets insets = balloon.getInsetsCopy();
1327       insets.bottom = balloon.getPointerLength(this);
1328       return new EmptyBorder(insets);
1329     }
1330
1331     @Override
1332     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
1333       rec.setLocation(targetPoint.x - rec.width / 2, targetPoint.y - rec.height);
1334     }
1335
1336     @Override
1337     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
1338       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
1339       return new Point(center.x, targetPoint.y - balloonSize.height);
1340     }
1341
1342     @Override
1343     protected Insets getTitleInsets(int normalInset, int pointerLength) {
1344       return new Insets(normalInset, normalInset, normalInset, normalInset);
1345     }
1346
1347     @Override
1348     protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
1349       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.BOTTOM);
1350       shaper.line(-balloon.getPointerWidth(this) / 2, -balloon.getPointerLength(this) + 1);
1351       shaper.toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown().toBottomCurve().line(0, 2)
1352         .roundLeftDown().lineTo(pointTarget.x + balloon.getPointerWidth(this) / 2, shaper.getCurrent().y)
1353         .lineTo(pointTarget.x, pointTarget.y)
1354         .close();
1355
1356
1357       return shaper.getShape();
1358     }
1359   }
1360
1361   private static class AtRight extends AbstractPosition {
1362     @Override
1363     public Point getShiftedPoint(Point targetPoint, int shift) {
1364       return new Point(targetPoint.x + shift, targetPoint.y);
1365     }
1366
1367     @Override
1368     public Point getShiftedPoint(Point targetPoint, Insets shift) {
1369       return getShiftedPoint(targetPoint, -shift.left);
1370     }
1371
1372     @Override
1373     int getChangeShift(AbstractPosition original, int xShift, int yShift) {
1374       return original == AT_LEFT ? xShift : 0;
1375     }
1376
1377     @Override
1378     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
1379       return rectangle.x - targetPoint.x;
1380     }
1381
1382     @Override
1383     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
1384       return new Rectangle(bounds.x + pointerLength, bounds.y, bounds.width - pointerLength, bounds.height);
1385     }
1386
1387     @Override
1388     EmptyBorder createBorder(final BalloonImpl balloon) {
1389       Insets insets = balloon.getInsetsCopy();
1390       insets.left += balloon.getPointerLength(this);
1391       return new EmptyBorder(insets);
1392     }
1393
1394     @Override
1395     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
1396       rec.setLocation(targetPoint.x, targetPoint.y - rec.height / 2);
1397     }
1398
1399     @Override
1400     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
1401       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
1402       return new Point(targetPoint.x, center.y);
1403     }
1404
1405     @Override
1406     protected Insets getTitleInsets(int normalInset, int pointerLength) {
1407       return new Insets(normalInset, pointerLength, normalInset, normalInset);
1408     }
1409
1410     @Override
1411     protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
1412       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.LEFT);
1413       shaper.line(balloon.getPointerLength(this), -balloon.getPointerWidth(this) / 2).toTopCurve().roundUpRight().toRightCurve()
1414         .roundRightDown()
1415         .toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp()
1416         .lineTo(shaper.getCurrent().x, pointTarget.y + balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
1417
1418       return shaper.getShape();
1419     }
1420   }
1421
1422   private static class AtLeft extends AbstractPosition {
1423     @Override
1424     public Point getShiftedPoint(Point targetPoint, int shift) {
1425       return new Point(targetPoint.x - shift, targetPoint.y);
1426     }
1427
1428     @Override
1429     public Point getShiftedPoint(Point targetPoint, Insets shift) {
1430       return getShiftedPoint(targetPoint, -shift.left);
1431     }
1432
1433     @Override
1434     int getChangeShift(AbstractPosition original, int xShift, int yShift) {
1435       return original == AT_RIGHT ? -xShift : 0;
1436     }
1437
1438
1439     @Override
1440     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
1441       return targetPoint.x - (int)rectangle.getMaxX();
1442     }
1443
1444     @Override
1445     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
1446       return new Rectangle(bounds.x, bounds.y, bounds.width - pointerLength, bounds.height);
1447     }
1448
1449     @Override
1450     EmptyBorder createBorder(final BalloonImpl balloon) {
1451       Insets insets = balloon.getInsetsCopy();
1452       insets.right += balloon.getPointerLength(this);
1453       return new EmptyBorder(insets);
1454     }
1455
1456     @Override
1457     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
1458       rec.setLocation(targetPoint.x - rec.width, targetPoint.y - rec.height / 2);
1459     }
1460
1461     @Override
1462     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
1463       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, JBUI.emptySize()), balloonSize);
1464       return new Point(targetPoint.x - balloonSize.width, center.y);
1465     }
1466
1467     @Override
1468     protected Insets getTitleInsets(int normalInset, int pointerLength) {
1469       return new Insets(normalInset, pointerLength, normalInset, normalInset);
1470     }
1471
1472     @Override
1473     protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) {
1474       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.RIGHT);
1475       shaper
1476         .lineTo((int)bounds.getMaxX() - shaper.getTargetDelta(SwingConstants.RIGHT) - 1, pointTarget.y + balloon.getPointerWidth(this) / 2);
1477       shaper.toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown()
1478         .lineTo(shaper.getCurrent().x, pointTarget.y - balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
1479       return shaper.getShape();
1480     }
1481   }
1482
1483   public interface ActionProvider {
1484     @NotNull
1485     List<ActionButton> createActions();
1486
1487     void layout(@NotNull Rectangle bounds);
1488   }
1489
1490   public class ActionButton extends NonOpaquePanel {
1491     private final Icon myIcon;
1492     private final Icon myHoverIcon;
1493     private final String myHint;
1494     private final Consumer<MouseEvent> myListener;
1495     protected final BaseButtonBehavior myButton;
1496
1497     public ActionButton(@NotNull Icon icon, @Nullable Icon hoverIcon, @Nullable String hint, @NotNull Consumer<MouseEvent> listener) {
1498       myIcon = icon;
1499       myHoverIcon = hoverIcon;
1500       myHint = hint;
1501       myListener = listener;
1502
1503       myButton = new BaseButtonBehavior(this, TimedDeadzone.NULL) {
1504         @Override
1505         protected void execute(MouseEvent e) {
1506           myListener.consume(e);
1507         }
1508       };
1509     }
1510
1511     @Override
1512     public Dimension getPreferredSize() {
1513       return new Dimension(myIcon.getIconWidth(), myIcon.getIconHeight());
1514     }
1515
1516     @Override
1517     protected void paintComponent(Graphics g) {
1518       super.paintComponent(g);
1519       if (hasPaint()) {
1520         if (myHoverIcon != null && myButton.isHovered()) {
1521           paintIcon(g, myHoverIcon);
1522           showHint(true);
1523         }
1524         else {
1525           paintIcon(g, myIcon);
1526           showHint(false);
1527         }
1528       }
1529     }
1530
1531     private void showHint(boolean show) {
1532       if (myHint != null) {
1533         ActionMenu.showDescriptionInStatusBar(true, this, show ? myHint : null);
1534       }
1535     }
1536
1537     public boolean hasPaint() {
1538       return getWidth() > 0 && myLastMoveWasInsideBalloon;
1539     }
1540
1541     protected void paintIcon(@NotNull Graphics g, @NotNull Icon icon) {
1542       icon.paintIcon(this, g, 0, 0);
1543     }
1544   }
1545
1546   private class CloseButton extends ActionButton {
1547     private CloseButton(@NotNull Consumer<MouseEvent> listener) {
1548       super(getCloseButton(), null, null, listener);
1549       setVisible(myEnableButtons);
1550     }
1551
1552     @Override
1553     protected void paintIcon(@NotNull Graphics g, @NotNull Icon icon) {
1554       if (myEnableButtons) {
1555         final boolean pressed = myButton.isPressedByMouse();
1556         icon.paintIcon(this, g, pressed ? 1 : 0, pressed ? 1 : 0);
1557       }
1558     }
1559   }
1560
1561   private class MyComponent extends JPanel implements ComponentWithMnemonics {
1562
1563     private BufferedImage myImage;
1564     private float myAlpha;
1565     private final BalloonImpl myBalloon;
1566
1567     private final JComponent myContent;
1568     private ShadowBorderPainter.Shadow myShadow;
1569
1570     private MyComponent(JComponent content, BalloonImpl balloon, EmptyBorder shapeBorder) {
1571       setOpaque(false);
1572       setLayout(null);
1573       myBalloon = balloon;
1574
1575       setFocusCycleRoot(true);
1576       putClientProperty(Balloon.KEY, BalloonImpl.this);
1577
1578       myContent = new JPanel(new BorderLayout(2, 2));
1579       Wrapper contentWrapper = new Wrapper(content);
1580       if (myTitle != null) {
1581         myTitleLabel = new JLabel(myTitle, SwingConstants.CENTER);
1582         myTitleLabel.setForeground(UIManager.getColor("List.background"));
1583         myTitleLabel.setBorder(new EmptyBorder(0, 4, 0, 4));
1584         myContent.add(myTitleLabel, BorderLayout.NORTH);
1585         contentWrapper.setBorder(new EmptyBorder(1, 1, 1, 1));
1586       }
1587       myContent.add(contentWrapper, BorderLayout.CENTER);
1588       myContent.setBorder(shapeBorder);
1589       myContent.setOpaque(false);
1590
1591       add(myContent);
1592     }
1593
1594     public Rectangle getContentBounds() {
1595       Rectangle bounds = super.getBounds();
1596       JBInsets.removeFrom(bounds, getInsets());
1597       return bounds;
1598     }
1599
1600     public void clear() {
1601       myImage = null;
1602       myAlpha = -1;
1603     }
1604
1605     @Override
1606     public void doLayout() {
1607       Rectangle bounds = new Rectangle(getWidth(), getHeight());
1608       JBInsets.removeFrom(bounds, getInsets());
1609
1610       myContent.setBounds(bounds);
1611     }
1612
1613     @Override
1614     public Dimension getPreferredSize() {
1615       return addInsets(myContent.getPreferredSize());
1616     }
1617
1618     @Override
1619     public Dimension getMinimumSize() {
1620       return addInsets(myContent.getMinimumSize());
1621     }
1622
1623     private Dimension addInsets(Dimension size) {
1624       JBInsets.addTo(size, getInsets());
1625       return size;
1626     }
1627
1628     @Override
1629     protected void paintChildren(Graphics g) {
1630       if (myImage == null || myAlpha == -1) {
1631         super.paintChildren(g);
1632       }
1633     }
1634
1635     private void paintChildrenImpl(Graphics g) {
1636       // Paint to an image without alpha to preserve fonts subpixel antialiasing
1637       @SuppressWarnings("UndesirableClassUsage")
1638       BufferedImage image = UIUtil.createImage(getWidth(), getHeight(),
1639                                                BufferedImage.TYPE_INT_RGB);//new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
1640       Graphics2D imageGraphics = image.createGraphics();
1641       //noinspection UseJBColor
1642       imageGraphics.setColor(new Color(myFillColor.getRGB())); // create a copy to remove alpha
1643       imageGraphics.fillRect(0, 0, getWidth(), getHeight());
1644
1645       super.paintChildren(imageGraphics);
1646       imageGraphics.dispose();
1647       Graphics2D g2d = (Graphics2D)g.create();
1648       try {
1649         if (UIUtil.isRetina()) {
1650           g2d.scale(.5, .5);
1651         }
1652         UIUtil.drawImage(g2d, makeColorTransparent(image, myFillColor), 0, 0, null);
1653       }
1654       finally {
1655         g2d.dispose();
1656       }
1657     }
1658
1659     private Image makeColorTransparent(Image image, Color color) {
1660       final int markerRGB = color.getRGB() | 0xFF000000;
1661       ImageFilter filter = new RGBImageFilter() {
1662         @Override
1663         public int filterRGB(int x, int y, int rgb) {
1664           if ((rgb | 0xFF000000) == markerRGB) {
1665             return 0x00FFFFFF & rgb; // set alpha to 0
1666           }
1667           else {
1668             return rgb;
1669           }
1670         }
1671       };
1672       return ImageUtil.filter(image, filter);
1673     }
1674
1675     @Override
1676     protected void paintComponent(final Graphics g) {
1677       super.paintComponent(g);
1678
1679       final Graphics2D g2d = (Graphics2D)g;
1680
1681       Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
1682       Rectangle shapeBounds = myContent.getBounds();
1683       int shadowSize = myBalloon.getShadowBorderSize();
1684
1685       if (shadowSize > 0 && myShadow == null && myShadowBorderProvider == null) {
1686         initComponentImage(pointTarget, shapeBounds);
1687         myShadow = ShadowBorderPainter.createShadow(myImage, 0, 0, false, shadowSize / 2);
1688       }
1689
1690       if (myImage == null && myAlpha != -1) {
1691         initComponentImage(pointTarget, shapeBounds);
1692       }
1693
1694       if (myShadowBorderProvider != null) {
1695         myShadowBorderProvider.paintShadow(this, g);
1696       }
1697
1698       if (myImage != null && myAlpha != -1) {
1699         g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, myAlpha));
1700         paintShadow(g);
1701         UIUtil.drawImage(g2d, myImage, 0, 0, null);
1702       }
1703       else {
1704         paintShadow(g);
1705         myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)g, pointTarget);
1706       }
1707     }
1708
1709     private void paintShadow(Graphics graphics) {
1710       if (myShadow != null) {
1711         if (UIUtil.isRetina()) {
1712           graphics = graphics.create();
1713           ((Graphics2D)graphics).scale(.5, .5);
1714         }
1715         UIUtil.drawImage(graphics, myShadow.getImage(), myShadow.getX(), myShadow.getY(), null);
1716       }
1717     }
1718
1719     @Override
1720     public boolean contains(int x, int y) {
1721       Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
1722       Rectangle bounds = myContent.getBounds();
1723       Shape shape;
1724       if (myShowPointer) {
1725         shape = myBalloon.myPosition.getPointingShape(bounds, pointTarget, myBalloon);
1726       }
1727       else {
1728         shape =
1729           new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, myBalloon.getArc(), myBalloon.getArc());
1730       }
1731       return shape.contains(x, y);
1732     }
1733
1734     private void initComponentImage(Point pointTarget, Rectangle shapeBounds) {
1735       if (myImage != null) return;
1736
1737       myImage = UIUtil.createImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
1738       Graphics2D imageGraphics = (Graphics2D)myImage.getGraphics();
1739       myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, imageGraphics, pointTarget);
1740       paintChildrenImpl(imageGraphics);
1741       imageGraphics.dispose();
1742     }
1743
1744
1745     @Override
1746     public void removeNotify() {
1747       super.removeNotify();
1748
1749       if (!ScreenUtil.isStandardAddRemoveNotify(this)) {
1750         return;
1751       }
1752
1753       final List<ActionButton> buttons = myActionButtons;
1754       myActionButtons = null;
1755       if (buttons != null) {
1756         //noinspection SSBasedInspection
1757         SwingUtilities.invokeLater(new Runnable() {
1758           @Override
1759           public void run() {
1760             for (ActionButton button : buttons) {
1761               disposeButton(button);
1762             }
1763           }
1764         });
1765       }
1766     }
1767
1768     public void setAlpha(float alpha) {
1769       myAlpha = alpha;
1770       paintImmediately(0, 0, getWidth(), getHeight());
1771     }
1772
1773     public void _setBounds(Rectangle bounds) {
1774       Rectangle currentBounds = getBounds();
1775       if (currentBounds.width != bounds.width || currentBounds.height != bounds.height) {
1776         invalidateShadowImage();
1777       }
1778
1779       super.setBounds(bounds);
1780
1781
1782       if (getParent() != null) {
1783         if (myActionButtons == null) {
1784           myActionButtons = myActionProvider.createActions();
1785         }
1786
1787         for (ActionButton button : myActionButtons) {
1788           if (button.getParent() == null) {
1789             myLayeredPane.add(button);
1790             myLayeredPane.setLayer(button, JLayeredPane.DRAG_LAYER);
1791           }
1792         }
1793       }
1794
1795       if (isVisible()) {
1796         Rectangle lpBounds = SwingUtilities.convertRectangle(getParent(), bounds, myLayeredPane);
1797         lpBounds = myPosition
1798           .getPointlessContentRec(lpBounds, myBalloon.myShadowBorderProvider == null ? myBalloon.getPointerLength(myPosition) : 0);
1799         myActionProvider.layout(lpBounds);
1800       }
1801
1802       if (isVisible()) {
1803         revalidate();
1804         repaint();
1805       }
1806     }
1807
1808     private void invalidateShadowImage() {
1809       myImage = null;
1810       myShadow = null;
1811     }
1812
1813     public void repaintButton() {
1814       if (myActionButtons != null) {
1815         for (ActionButton button : myActionButtons) {
1816           button.repaint();
1817         }
1818       }
1819     }
1820   }
1821
1822   private static class Shaper {
1823     private final GeneralPath myPath = new GeneralPath();
1824
1825     Rectangle myBounds;
1826     @JdkConstants.TabPlacement
1827     private final int myTargetSide;
1828     private final BalloonImpl myBalloon;
1829
1830     public Shaper(BalloonImpl balloon, Rectangle bounds, Point targetPoint, @JdkConstants.TabPlacement int targetSide) {
1831       myBalloon = balloon;
1832       myBounds = bounds;
1833       myTargetSide = targetSide;
1834       start(targetPoint);
1835     }
1836
1837     private void start(Point start) {
1838       myPath.moveTo(start.x, start.y);
1839     }
1840
1841     public Shaper roundUpRight() {
1842       myPath.quadTo(getCurrent().x, getCurrent().y - myBalloon.getArc(), getCurrent().x + myBalloon.getArc(),
1843                     getCurrent().y - myBalloon.getArc());
1844       return this;
1845     }
1846
1847     public Shaper roundRightDown() {
1848       myPath.quadTo(getCurrent().x + myBalloon.getArc(), getCurrent().y, getCurrent().x + myBalloon.getArc(),
1849                     getCurrent().y + myBalloon.getArc());
1850       return this;
1851     }
1852
1853     public Shaper roundLeftUp() {
1854       myPath.quadTo(getCurrent().x - myBalloon.getArc(), getCurrent().y, getCurrent().x - myBalloon.getArc(),
1855                     getCurrent().y - myBalloon.getArc());
1856       return this;
1857     }
1858
1859     public Shaper roundLeftDown() {
1860       myPath.quadTo(getCurrent().x, getCurrent().y + myBalloon.getArc(), getCurrent().x - myBalloon.getArc(),
1861                     getCurrent().y + myBalloon.getArc());
1862       return this;
1863     }
1864
1865     public Point getCurrent() {
1866       return new Point((int)myPath.getCurrentPoint().getX(), (int)myPath.getCurrentPoint().getY());
1867     }
1868
1869     public Shaper line(final int deltaX, final int deltaY) {
1870       myPath.lineTo(getCurrent().x + deltaX, getCurrent().y + deltaY);
1871       return this;
1872     }
1873
1874     public Shaper lineTo(final int x, final int y) {
1875       myPath.lineTo(x, y);
1876       return this;
1877     }
1878
1879     private int getTargetDelta(@JdkConstants.TabPlacement int effectiveSide) {
1880       return effectiveSide == myTargetSide ? myBalloon.getPointerLength(myBalloon.myPosition) : 0;
1881     }
1882
1883     public Shaper toRightCurve() {
1884       myPath.lineTo((int)myBounds.getMaxX() - myBalloon.getArc() - getTargetDelta(SwingConstants.RIGHT) - 1, getCurrent().y);
1885       return this;
1886     }
1887
1888     public Shaper toBottomCurve() {
1889       myPath.lineTo(getCurrent().x, (int)myBounds.getMaxY() - myBalloon.getArc() - getTargetDelta(SwingConstants.BOTTOM) - 1);
1890       return this;
1891     }
1892
1893     public Shaper toLeftCurve() {
1894       myPath.lineTo((int)myBounds.getX() + myBalloon.getArc() + getTargetDelta(SwingConstants.LEFT), getCurrent().y);
1895       return this;
1896     }
1897
1898     public Shaper toTopCurve() {
1899       myPath.lineTo(getCurrent().x, (int)myBounds.getY() + myBalloon.getArc() + getTargetDelta(SwingConstants.TOP));
1900       return this;
1901     }
1902
1903     public void close() {
1904       myPath.closePath();
1905     }
1906
1907     public Shape getShape() {
1908       return myPath;
1909     }
1910   }
1911
1912   @Override
1913   public boolean wasFadedIn() {
1914     return myFadedIn;
1915   }
1916
1917   @Override
1918   public boolean wasFadedOut() {
1919     return myFadedOut;
1920   }
1921
1922   @Override
1923   public boolean isDisposed() {
1924     return myDisposed;
1925   }
1926
1927   @Override
1928   public void setTitle(String title) {
1929     myTitleLabel.setText(title);
1930   }
1931
1932   public void setActionProvider(@NotNull ActionProvider actionProvider) {
1933     myActionProvider = actionProvider;
1934   }
1935
1936   @Override
1937   public RelativePoint getShowingPoint() {
1938     Point p = myPosition.getShiftedPoint(myTargetPoint, myCalloutShift * -1);
1939     return new RelativePoint(myLayeredPane, p);
1940   }
1941
1942   @Override
1943   public void setAnimationEnabled(boolean enabled) {
1944     myAnimationEnabled = enabled;
1945   }
1946
1947   public boolean isAnimationEnabled() {
1948     return myAnimationEnabled && myAnimationCycle > 0 && !RemoteDesktopDetector.isRemoteSession();
1949   }
1950
1951   public boolean isBlockClicks() {
1952     return myBlockClicks;
1953   }
1954 }