added internal action to dump all extensions
[idea/community.git] / platform / platform-impl / src / com / intellij / ui / BalloonImpl.java
1 /*
2  * Copyright 2000-2009 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.openapi.Disposable;
19 import com.intellij.openapi.ui.MessageType;
20 import com.intellij.openapi.ui.popup.Balloon;
21 import com.intellij.openapi.ui.popup.JBPopupListener;
22 import com.intellij.openapi.ui.popup.LightweightWindow;
23 import com.intellij.openapi.ui.popup.LightweightWindowEvent;
24 import com.intellij.openapi.util.Disposer;
25 import com.intellij.openapi.util.IconLoader;
26 import com.intellij.openapi.util.Ref;
27 import com.intellij.openapi.wm.impl.content.GraphicsConfig;
28 import com.intellij.ui.awt.RelativePoint;
29 import com.intellij.ui.components.panels.NonOpaquePanel;
30 import com.intellij.ui.components.panels.Wrapper;
31 import com.intellij.util.Alarm;
32 import com.intellij.util.IJSwingUtilities;
33 import com.intellij.util.Range;
34 import com.intellij.util.containers.HashSet;
35 import com.intellij.util.ui.*;
36 import org.jetbrains.annotations.Nullable;
37
38 import javax.swing.*;
39 import javax.swing.border.EmptyBorder;
40 import javax.swing.border.LineBorder;
41 import java.awt.*;
42 import java.awt.event.*;
43 import java.awt.geom.GeneralPath;
44 import java.awt.geom.Rectangle2D;
45 import java.awt.geom.RoundRectangle2D;
46 import java.awt.image.BufferedImage;
47 import java.util.Set;
48 import java.util.concurrent.CopyOnWriteArraySet;
49
50 public class BalloonImpl implements Disposable, Balloon, LightweightWindow, PositionTracker.Client<Balloon> {
51
52   private MyComponent myComp;
53   private JLayeredPane myLayeredPane;
54   private Position myPosition;
55   private Point myTargetPoint;
56   private final boolean myHideOnFrameResize;
57
58   private final Color myBorderColor;
59   private final Color myFillColor;
60
61   private final Insets myContainerInsets = new Insets(2, 2, 2, 2);
62
63   private boolean myLastMoveWasInsideBalloon;
64
65   private Rectangle myForcedBounds;
66
67   private CloseButton myCloseRec;
68
69   private final AWTEventListener myAwtActivityListener = new AWTEventListener() {
70     public void eventDispatched(final AWTEvent event) {
71       if (myHideOnMouse &&
72           (event.getID() == MouseEvent.MOUSE_PRESSED)) {
73         final MouseEvent me = (MouseEvent)event;
74         if (isInsideBalloon(me))  return;
75
76         hide();
77         return;
78       }
79
80       if (myClickHandler != null && event.getID() == MouseEvent.MOUSE_CLICKED) {
81         final MouseEvent me = (MouseEvent)event;
82         if (!(me.getComponent() instanceof CloseButton) && isInsideBalloon(me)) {
83           myClickHandler.actionPerformed(new ActionEvent(BalloonImpl.this, ActionEvent.ACTION_PERFORMED, "click", me.getModifiersEx()));
84           if (myCloseOnClick) {
85             hide();
86             return;
87           }
88         }
89       }
90
91       if (myEnableCloseButton && event.getID() == MouseEvent.MOUSE_MOVED) {
92         final MouseEvent me = (MouseEvent)event;
93         final boolean inside = isInsideBalloon(me);
94         final boolean moveChanged = inside != myLastMoveWasInsideBalloon;
95         myLastMoveWasInsideBalloon = inside;
96         if (moveChanged) {
97           myComp.repaintButton();
98         }
99       }
100
101       if (event instanceof MouseEvent && UIUtil.isCloseClick((MouseEvent)event)) {
102         hide();
103         return;
104       }
105
106       if (myHideOnKey && (event.getID() == KeyEvent.KEY_PRESSED)) {
107         final KeyEvent ke = (KeyEvent)event;
108         if (ke.getKeyCode() != KeyEvent.VK_SHIFT && ke.getKeyCode() != KeyEvent.VK_CONTROL && ke.getKeyCode() != KeyEvent.VK_ALT && ke.getKeyCode() != KeyEvent.VK_META) {
109           if (SwingUtilities.isDescendingFrom(ke.getComponent(), myComp) || ke.getComponent() == myComp) return;
110           hide();
111         }
112       }
113     }
114   };
115   private final long myFadeoutTime;
116   private Dimension myDefaultPrefSize;
117   private final ActionListener myClickHandler;
118   private final boolean myCloseOnClick;
119
120   private final CopyOnWriteArraySet<JBPopupListener> myListeners = new CopyOnWriteArraySet<JBPopupListener>();
121   private boolean myVisible;
122   private PositionTracker<Balloon> myTracker;
123   private int myAnimationCycle = 500;
124
125   private boolean myFadedIn;
126   private boolean myFadedOut;
127   private int myCalloutshift;
128
129   private boolean isInsideBalloon(MouseEvent me) {
130     if (!me.getComponent().isShowing()) return true;
131     if (SwingUtilities.isDescendingFrom(me.getComponent(), myComp) || me.getComponent() == myComp) return true;
132
133
134     final Point mouseEventPoint = me.getPoint();
135     SwingUtilities.convertPointToScreen(mouseEventPoint, me.getComponent());
136
137     if (!myComp.isShowing()) return false;
138
139     final Rectangle compRect = new Rectangle(myComp.getLocationOnScreen(), myComp.getSize());
140     if (compRect.contains(mouseEventPoint)) return true;
141     return false;
142   }
143
144   private final ComponentAdapter myComponentListener = new ComponentAdapter() {
145     public void componentResized(final ComponentEvent e) {
146       if (myHideOnFrameResize) {
147         hide();
148       }
149     }
150   };
151   private Animator myAnimator;
152   private boolean myShowPointer;
153
154   private boolean myDisposed;
155   private final JComponent myContent;
156   private final boolean myHideOnMouse;
157   private final boolean myHideOnKey;
158   private final boolean myEnableCloseButton;
159   private final Icon myCloseButton = IconLoader.getIcon("/general/balloonClose.png");
160
161   public BalloonImpl(JComponent content,
162                      Color borderColor,
163                      Color fillColor,
164                      boolean hideOnMouse,
165                      boolean hideOnKey,
166                      boolean showPointer,
167                      boolean enableCloseButton,
168                      long fadeoutTime,
169                      boolean hideOnFrameResize,
170                      ActionListener clickHandler,
171                      boolean closeOnClick,
172                      int animationCycle,
173                      int calloutShift) {
174     myBorderColor = borderColor;
175     myFillColor = fillColor;
176     myContent = content;
177     myHideOnMouse = hideOnMouse;
178     myHideOnKey = hideOnKey;
179     myShowPointer = showPointer;
180     myEnableCloseButton = enableCloseButton;
181     myHideOnFrameResize = hideOnFrameResize;
182     myClickHandler = clickHandler;
183     myCloseOnClick = closeOnClick;
184     myCalloutshift = calloutShift;
185
186     myFadeoutTime = fadeoutTime;
187     myAnimationCycle = animationCycle;
188   }
189
190   public void show(final RelativePoint target, final Balloon.Position position) {
191     Position pos = BELOW;
192     switch (position) {
193       case atLeft:
194         pos = AT_LEFT;
195         break;
196       case atRight:
197         pos = AT_RIGHT;
198         break;
199       case below:
200         pos = BELOW;
201         break;
202       case above:
203         pos = ABOVE;
204         break;
205     }
206
207     show(target, pos);
208   }
209
210   public void show(PositionTracker<Balloon> tracker, Balloon.Position position) {
211     Position pos = BELOW;
212     switch (position) {
213       case atLeft:
214         pos = AT_LEFT;
215         break;
216       case atRight:
217         pos = AT_RIGHT;
218         break;
219       case below:
220         pos = BELOW;
221         break;
222       case above:
223         pos = ABOVE;
224         break;
225     }
226
227     show(tracker, pos);
228   }
229
230
231   private void show(RelativePoint target, Position position) {
232     show(new PositionTracker.Static<Balloon>(target), position);
233   }
234
235   private void show(PositionTracker<Balloon> tracker, Position position) {
236     if (isVisible()) return;
237
238     assert !myDisposed : "Balloon is already disposed";
239     assert tracker.getComponent().isShowing() : "Target component is not showing: " + tracker;
240
241     myTracker = tracker;
242     myTracker.init(this);
243
244     JRootPane root = null;
245     JDialog dialog = IJSwingUtilities.findParentOfType(tracker.getComponent(), JDialog.class);
246     if (dialog != null) {
247       root = dialog.getRootPane();
248     } else {
249       JFrame frame = IJSwingUtilities.findParentOfType(tracker.getComponent(), JFrame.class);
250       if (frame != null) {
251         root = frame.getRootPane();
252       } else {
253         assert false;
254       }
255     }
256
257     myVisible = true;
258
259     myLayeredPane = root.getLayeredPane();
260     myPosition = position;
261
262     myLayeredPane.addComponentListener(myComponentListener);
263
264     myTargetPoint = myTracker.recalculateLocation(this).getPoint(myLayeredPane);
265
266
267     if (myShowPointer) {
268       Rectangle rec = getRecForPosition(myPosition, true);
269
270       if (!myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc(), getNormalInset())) {
271         rec = getRecForPosition(myPosition, false);
272
273         Rectangle lp = new Rectangle(new Point(myContainerInsets.left, myContainerInsets.top), myLayeredPane.getSize());
274         lp.width -= myContainerInsets.right;
275         lp.height -= myContainerInsets.bottom;
276
277         if (!lp.contains(rec)) {
278           Rectangle2D currentSquare = lp.createIntersection(rec);
279
280           double maxSquare = currentSquare.getWidth() * currentSquare.getHeight();
281           Position targetPosition = myPosition;
282
283           for (Position eachPosition : myPosition.getOtherPositions()) {
284             Rectangle2D eachIntersection = lp.createIntersection(getRecForPosition(eachPosition, false));
285             double eachSquare = eachIntersection.getWidth() * eachIntersection.getHeight();
286             if (maxSquare < eachSquare) {
287               maxSquare = eachSquare;
288               targetPosition = eachPosition;
289             }
290           }
291
292           myPosition = targetPosition;
293         }
294       }
295     }
296
297     createComponent();
298
299     myComp.validate();
300
301     Rectangle rec = myComp.getBounds();
302
303     if (myShowPointer && !myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc(), getNormalInset())) {
304       myShowPointer = false;
305       myComp.removeAll();
306       myLayeredPane.remove(myComp);
307
308       myForcedBounds = rec;
309       createComponent();
310     }
311
312     for (JBPopupListener each : myListeners) {
313       each.beforeShown(new LightweightWindowEvent(this));
314     }
315
316     runAnimation(true, myLayeredPane);
317
318     myLayeredPane.revalidate();
319     myLayeredPane.repaint();
320
321
322     Toolkit.getDefaultToolkit().addAWTEventListener(myAwtActivityListener, MouseEvent.MOUSE_EVENT_MASK |
323                                                                            MouseEvent.MOUSE_MOTION_EVENT_MASK |
324                                                                            KeyEvent.KEY_EVENT_MASK);
325   }
326
327   private Rectangle getRecForPosition(Position position, boolean adjust) {
328     Dimension size = getContentSizeFor(position);
329
330     Rectangle rec = new Rectangle(new Point(0, 0), size);
331
332     position.setRecToRelativePosition(rec, position.getShiftedPoint(myTargetPoint, myCalloutshift));
333
334     if (adjust) {
335       rec = myPosition
336         .getUpdatedBounds(myLayeredPane.getSize(), myForcedBounds, rec.getSize(), myShowPointer, myTargetPoint, myContainerInsets, myCalloutshift);
337     }
338
339     return rec;
340   }
341
342   private Dimension getContentSizeFor(Position position) {
343     Insets insets = position.createBorder(this).getBorderInsets();
344     if (insets == null) {
345       insets = new Insets(0, 0, 0, 0);
346     }
347
348     Dimension size = myContent.getPreferredSize();
349     size.width += insets.left + insets.right;
350     size.height += insets.top + insets.bottom;
351
352     return size;
353   }
354
355   private void createComponent() {
356     myComp = new MyComponent(myContent, this, myShowPointer
357                                ? myPosition.createBorder(this)
358                                : getPointlessBorder());
359
360
361     myComp.clear();
362     myComp.myAlpha = 0f;
363
364
365     myLayeredPane.add(myComp, JLayeredPane.POPUP_LAYER);
366     myPosition.updateBounds(this);
367   }
368
369
370   private EmptyBorder getPointlessBorder() {
371     return new EmptyBorder(getNormalInset(), getNormalInset(), getNormalInset(), getNormalInset());
372   }
373
374   public void revalidate(PositionTracker<Balloon> tracker) {
375     RelativePoint newPosition = tracker.recalculateLocation(this);
376
377     if (newPosition != null) {
378       myTargetPoint = newPosition.getPoint(myLayeredPane);
379       myPosition.updateBounds(this);
380     }
381   }
382
383   public void show(JLayeredPane pane) {
384     show(pane, null);
385   }
386
387   public void show(JLayeredPane pane, @Nullable Rectangle bounds) {
388     if (bounds != null) {
389       myForcedBounds = bounds;
390     }
391     show(new RelativePoint(pane, new Point(0, 0)), Balloon.Position.above);
392   }
393
394
395   private void runAnimation(boolean forward, final JLayeredPane layeredPane) {
396     if (myAnimator != null) {
397       Disposer.dispose(myAnimator);
398     }
399     myAnimator = new Animator("Balloon", 10, myAnimationCycle, false, 0, 1, forward) {
400       public void paintNow(final float frame, final float totalFrames, final float cycle) {
401         if (myComp.getParent() == null) return;
402         myComp.setAlpha(frame / totalFrames);
403       }
404
405       @Override
406       protected void paintCycleEnd() {
407         if (myComp.getParent() == null) return;
408
409         if (isForward()) {
410           myComp.clear();
411           myComp.repaint();
412
413           myFadedIn = true;
414
415           startFadeoutTimer();
416         }
417         else {
418           layeredPane.remove(myComp);
419           layeredPane.revalidate();
420           layeredPane.repaint();
421         }
422         Disposer.dispose(this);
423       }
424
425       @Override
426       public void dispose() {
427         super.dispose();
428         myAnimator = null;
429       }
430     };
431
432     myAnimator.setTakInitialDelay(false);
433     myAnimator.resume();
434   }
435
436   private void startFadeoutTimer() {
437     if (myFadeoutTime > 0) {
438       Alarm fadeoutAlarm = new Alarm(this);
439       fadeoutAlarm.addRequest(new Runnable() {
440         public void run() {
441           hide();
442         }
443       }, (int)myFadeoutTime, null);
444     }
445   }
446
447
448   int getArc() {
449     return 3;
450   }
451
452   int getPointerWidth(Position position) {
453     return position.isTopBottomPointer() ? 14 : 11;
454   }
455
456   int getNormalInset() {
457     return 3;
458   }
459
460   int getPointerLength(Position position) {
461     return position.isTopBottomPointer() ? 10 : 8;
462   }
463
464   public void hide() {
465     Disposer.dispose(this);
466
467
468     for (JBPopupListener each : myListeners) {
469       each.onClosed(new LightweightWindowEvent(this));
470     }
471
472     myFadedOut = true;
473   }
474
475   public void addListener(JBPopupListener listener) {
476     myListeners.add(listener);
477   }
478
479   public void dispose() {
480     if (myDisposed) return;
481
482     Disposer.dispose(this);
483
484     myDisposed = true;
485
486     Toolkit.getDefaultToolkit().removeAWTEventListener(myAwtActivityListener);
487     if (myLayeredPane != null) {
488       myLayeredPane.removeComponentListener(myComponentListener);
489       runAnimation(false, myLayeredPane);
490     }
491
492
493     myVisible = false;
494
495     onDisposed();
496   }
497
498   protected void onDisposed() {
499
500   }
501
502   public boolean isVisible() {
503     return myVisible;
504   }
505
506   public void setShowPointer(final boolean show) {
507     myShowPointer = show;
508   }
509
510   public Icon getCloseButton() {
511     return myCloseButton;
512   }
513
514   public void setBounds(Rectangle bounds) {
515     myForcedBounds = bounds;
516     if (myPosition != null) {
517       myPosition.updateBounds(this);
518     }
519   }
520
521   public Dimension getPreferredSize() {
522     if (myComp != null) {
523       return myComp.getPreferredSize();
524     } else {
525       if (myDefaultPrefSize == null) {
526         final EmptyBorder border = getPointlessBorder();
527         final MyComponent c = new MyComponent(myContent, this, border);
528         myDefaultPrefSize = c.getPreferredSize();
529       }
530       return myDefaultPrefSize;
531     }
532   }
533
534   public abstract static class Position {
535
536     abstract EmptyBorder createBorder(final BalloonImpl balloon);
537
538
539     abstract void setRecToRelativePosition(Rectangle rec, Point targetPoint);
540
541
542     public void updateBounds(final BalloonImpl balloon) {
543       balloon.myComp._setBounds(getUpdatedBounds(balloon.myLayeredPane.getSize(),
544                                                  balloon.myForcedBounds,
545                                                  balloon.myComp.getPreferredSize(),
546                                                  balloon.myShowPointer,
547                                                  balloon.myTargetPoint,
548                                                  balloon.myContainerInsets,
549                                                  balloon.myCalloutshift));
550     }
551
552     public Rectangle getUpdatedBounds(Dimension layeredPaneSize,
553                                       Rectangle forcedBounds,
554                                       Dimension preferredSize,
555                                       boolean showPointer,
556                                       Point targetPoint, Insets containerInsets, int calloutShift) {
557
558       Rectangle bounds = forcedBounds;
559
560       Point point = showPointer ? getShiftedPoint(targetPoint, calloutShift) : targetPoint;
561
562       if (bounds == null) {
563         Point location = showPointer
564                          ? getLocation(layeredPaneSize, point, preferredSize)
565                          : new Point(point.x - preferredSize.width / 2, point.y - preferredSize.height / 2);
566         bounds = new Rectangle(location.x, location.y, preferredSize.width, preferredSize.height);
567
568         ScreenUtil.moveToFit(bounds, new Rectangle(0, 0, layeredPaneSize.width, layeredPaneSize.height), containerInsets);
569       }
570
571       return bounds;
572     }
573
574     abstract Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize);
575
576     void paintComponent(BalloonImpl balloon, final Rectangle bounds, final Graphics2D g, Point pointTarget) {
577       final GraphicsConfig cfg = new GraphicsConfig(g);
578       cfg.setAntialiasing(true);
579
580       Shape shape;
581       if (balloon.myShowPointer) {
582         shape = getPointingShape(bounds, g, pointTarget, balloon);
583       }
584       else {
585         shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, balloon.getArc(), balloon.getArc());
586       }
587
588       g.setColor(balloon.myFillColor);
589       g.fill(shape);
590       g.setColor(balloon.myBorderColor);
591       g.draw(shape);
592       cfg.restore();
593     }
594
595     protected abstract Shape getPointingShape(final Rectangle bounds,
596                                               final Graphics2D g,
597                                               final Point pointTarget,
598                                               final BalloonImpl balloon);
599
600     public boolean isOkToHavePointer(Point targetPoint, Rectangle bounds, int pointerLength, int pointerWidth, int arc, int normalInset) {
601       if (bounds.x < targetPoint.x && bounds.x + bounds.width > targetPoint.x && bounds.y < targetPoint.y && bounds.y + bounds.height < targetPoint.y) return false;
602
603       Rectangle pointless = getPointlessContentRec(bounds, pointerLength);
604
605       int size = getDistanceToTarget(pointless, targetPoint);
606       if (size < pointerLength) return false;
607
608       Range<Integer> balloonRange;
609       Range<Integer> pointerRange;
610       if (isTopBottomPointer()) {
611         balloonRange = new Range<Integer>(bounds.x + arc, bounds.x + bounds.width - arc * 2);
612         pointerRange = new Range<Integer>(targetPoint.x - pointerWidth / 2, targetPoint.x + pointerWidth / 2);
613       } else {
614         balloonRange = new Range<Integer>(bounds.y + arc, bounds.y + bounds.height - arc * 2);
615         pointerRange = new Range<Integer>(targetPoint.y - pointerWidth / 2, targetPoint.y + pointerWidth / 2);
616       }
617
618       return balloonRange.isWithin(pointerRange.getFrom()) && balloonRange.isWithin(pointerRange.getTo());
619     }
620
621     protected abstract int getDistanceToTarget(Rectangle rectangle, Point targetPoint);
622
623     protected boolean isTopBottomPointer() {
624       return this instanceof Below || this instanceof Above;
625     }
626
627     protected abstract Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength);
628
629     public Set<Position> getOtherPositions() {
630       HashSet<Position> all = new HashSet<Position>();
631       all.add(BELOW);
632       all.add(ABOVE);
633       all.add(AT_RIGHT);
634       all.add(AT_LEFT);
635
636       all.remove(this);
637
638       return all;
639     }
640
641     public abstract Point getShiftedPoint(Point targetPoint, int shift);
642   }
643
644   public static final Position BELOW = new Below();
645   public static final Position ABOVE = new Above();
646   public static final Position AT_RIGHT = new AtRight();
647   public static final Position AT_LEFT = new AtLeft();
648
649
650   private static class Below extends Position {
651
652
653     @Override
654     public Point getShiftedPoint(Point targetPoint, int shift) {
655       return new Point(targetPoint.x, targetPoint.y + shift);
656     }
657
658     @Override
659     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
660       return rectangle.y - targetPoint.y;
661     }
662
663     @Override
664     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
665       return new Rectangle(bounds.x, bounds.y + pointerLength, bounds.width, bounds.height - pointerLength);
666     }
667
668     EmptyBorder createBorder(final BalloonImpl balloon) {
669       return new EmptyBorder(balloon.getPointerLength(this) + balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset());
670     }
671
672     @Override
673     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
674       rec.setLocation(new Point(targetPoint.x - rec.width / 2, targetPoint.y));
675     }
676
677     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
678       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
679       return new Point(center.x, targetPoint.y);
680     }
681
682     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
683       bounds.y += balloon.getPointerLength(this);
684       bounds.height -= balloon.getPointerLength(this) - 1;
685     }
686
687     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
688       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.TOP);
689       shaper.line(balloon.getPointerWidth(this) / 2, balloon.getPointerLength(this)).toRightCurve().roundRightDown().toBottomCurve().roundLeftDown()
690         .toLeftCurve().roundLeftUp().toTopCurve().roundUpRight()
691         .lineTo(pointTarget.x - balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y);
692       shaper.close();
693
694       return shaper.getShape();
695     }
696
697     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
698       bounds.y += balloon.getPointerLength(this);
699       bounds.height += balloon.getPointerLength(this);
700       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
701     }
702
703   }
704
705   private static class Above extends Position {
706
707     @Override
708     public Point getShiftedPoint(Point targetPoint, int shift) {
709       return new Point(targetPoint.x, targetPoint.y - shift);
710     }
711
712     @Override
713     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
714       return targetPoint.y - (int)rectangle.getMaxY();
715     }
716
717     @Override
718     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
719       return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height - pointerLength);
720     }
721
722     EmptyBorder createBorder(final BalloonImpl balloon) {
723       return new EmptyBorder(balloon.getNormalInset(),
724                              balloon.getNormalInset(),
725                              balloon.getPointerLength(this),
726                              balloon.getNormalInset());
727     }
728
729     @Override
730     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
731       rec.setLocation(targetPoint.x - rec.width / 2, targetPoint.y - rec.height);
732     }
733
734     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
735       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
736       return new Point(center.x, targetPoint.y - balloonSize.height);
737     }
738
739     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
740       bounds.height -= balloon.getPointerLength(this) - 1;
741     }
742
743     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
744       bounds.y -= balloon.getPointerLength(this);
745       bounds.height -= balloon.getPointerLength(this);
746       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
747     }
748
749     @Override
750     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
751       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.BOTTOM);
752       shaper.line(-balloon.getPointerWidth(this) / 2, -balloon.getPointerLength(this) + 1);
753       shaper.toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown().toBottomCurve().line(0, 2)
754         .roundLeftDown().lineTo(pointTarget.x + balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y)
755         .close();
756
757
758       return shaper.getShape();
759     }
760   }
761
762   private static class AtRight extends Position {
763
764     @Override
765     public Point getShiftedPoint(Point targetPoint, int shift) {
766       return new Point(targetPoint.x + shift, targetPoint.y);
767     }
768
769     @Override
770     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
771       return rectangle.x - targetPoint.x;
772     }
773
774     @Override
775     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
776       return new Rectangle(bounds.x + pointerLength, bounds.y, bounds.width - pointerLength, bounds.height);
777     }
778
779     EmptyBorder createBorder(final BalloonImpl balloon) {
780       return new EmptyBorder(balloon.getNormalInset(), balloon.getPointerLength(this) + balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset());
781     }
782
783     @Override
784     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
785       rec.setLocation(targetPoint.x, targetPoint.y - rec.height / 2);
786     }
787
788     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
789       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
790       return new Point(targetPoint.x, center.y);
791     }
792
793     @Override
794     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
795       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.LEFT);
796       shaper.line(balloon.getPointerLength(this), -balloon.getPointerWidth(this) / 2).toTopCurve().roundUpRight().toRightCurve().roundRightDown()
797         .toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp()
798         .lineTo(shaper.getCurrent().x, pointTarget.y + balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
799
800       return shaper.getShape();
801     }
802
803     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
804       bounds.x += balloon.getPointerLength(this);
805       bounds.width -= balloon.getPointerLength(this);
806     }
807
808     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
809       bounds.x += balloon.getPointerLength(this);
810       bounds.width -= balloon.getPointerLength(this);
811       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
812     }
813   }
814
815   private static class AtLeft extends Position {
816
817     @Override
818     public Point getShiftedPoint(Point targetPoint, int shift) {
819       return new Point(targetPoint.x - shift, targetPoint.y);
820     }
821
822     @Override
823     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
824       return targetPoint.x - (int)rectangle.getMaxX();
825     }
826
827     @Override
828     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
829       return new Rectangle(bounds.x, bounds.y, bounds.width - pointerLength, bounds.height);
830     }
831
832     EmptyBorder createBorder(final BalloonImpl balloon) {
833       return new EmptyBorder(balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset(), balloon.getPointerLength(this) + balloon.getNormalInset());
834     }
835
836     @Override
837     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
838       rec.setLocation(targetPoint.x - rec.width, targetPoint.y - rec.height / 2);
839     }
840
841     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
842       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
843       return new Point(targetPoint.x - balloonSize.width, center.y);
844     }
845
846     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
847       bounds.width -= balloon.getPointerLength(this);
848     }
849
850     @Override
851     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
852       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.RIGHT);
853       shaper.line(-balloon.getPointerLength(this), balloon.getPointerWidth(this) / 2);
854       shaper.toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown()
855         .lineTo(shaper.getCurrent().x, pointTarget.y - balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
856       return shaper.getShape();
857     }
858
859     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
860       bounds.width -= balloon.getPointerLength(this);
861       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
862     }
863   }
864
865   private class CloseButton extends NonOpaquePanel {
866
867     private BaseButtonBehavior myButton;
868
869     private CloseButton() {
870       myButton = new BaseButtonBehavior(this, TimedDeadzone.NULL) {
871         protected void execute(MouseEvent e) {
872           //noinspection SSBasedInspection
873           SwingUtilities.invokeLater(new Runnable() {
874             public void run() {
875               BalloonImpl.this.hide();
876             }
877           });
878         }
879       };
880
881     }
882
883     @Override
884     protected void paintComponent(Graphics g) {
885       super.paintComponent(g);
886
887       if (!myEnableCloseButton) return;
888
889       if (getWidth() > 0 && myLastMoveWasInsideBalloon) {
890         final boolean pressed = myButton.isPressedByMouse();
891         getCloseButton().paintIcon(this, g, (pressed ? 1 : 0), (pressed ? 1 : 0));
892       }
893     }
894   }
895
896   private class MyComponent extends JPanel {
897
898     private BufferedImage myImage;
899     private float myAlpha;
900     private final BalloonImpl myBalloon;
901
902     private final Wrapper myContent;
903
904     private MyComponent(JComponent content, BalloonImpl balloon, EmptyBorder shapeBorder) {
905       setOpaque(false);
906       setLayout(null);
907       myBalloon = balloon;
908
909       myContent = new Wrapper(content);
910       myContent.setBorder(shapeBorder);
911       myContent.setOpaque(false);
912
913       add(myContent);
914
915       myCloseRec = new CloseButton();
916     }
917
918     public void clear() {
919       myImage = null;
920       myAlpha = -1;
921     }
922
923     @Override
924     public void doLayout() {
925       Insets insets = getInsets();
926       if (insets == null) {
927         insets = new Insets(0, 0, 0, 0);
928       }
929
930       myContent.setBounds(insets.left, insets.top, getWidth() - insets.left - insets.right, getHeight() - insets.top - insets.bottom);
931     }
932
933     @Override
934     public Dimension getPreferredSize() {
935       return addInsets(myContent.getPreferredSize());
936     }
937
938     @Override
939     public Dimension getMinimumSize() {
940       return addInsets(myContent.getMinimumSize());
941     }
942
943     private Dimension addInsets(Dimension size) {
944       final Insets insets = getInsets();
945       if (insets != null) {
946         size.width += (insets.left + insets.right);
947         size.height += (insets.top + insets.bottom);
948       }
949
950       return size;
951     }
952
953     @Override
954     protected void paintComponent(final Graphics g) {
955       super.paintComponent(g);
956
957       final Graphics2D g2d = (Graphics2D)g;
958
959       final Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
960
961       Rectangle shapeBounds = myContent.getBounds();
962
963       if (myImage == null && myAlpha != -1) {
964         myImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
965         myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)myImage.getGraphics(), pointTarget);
966       }
967
968       if (myImage != null && myAlpha != -1) {
969         g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, myAlpha));
970
971         g2d.drawImage(myImage, 0, 0, null);
972       }
973       else {
974         myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)g, pointTarget);
975       }
976     }
977
978
979     @Override
980     public void removeNotify() {
981       super.removeNotify();
982
983       if (myLayeredPane != null) {
984         final JLayeredPane pane = myLayeredPane;
985         SwingUtilities.invokeLater(new Runnable() {
986           @Override
987           public void run() {
988             pane.remove(myCloseRec);
989           }
990         });
991       }
992     }
993
994     public void setAlpha(float alpha) {
995       myAlpha = alpha;
996       paintImmediately(0, 0, getWidth(), getHeight());
997     }
998
999     public void _setBounds(Rectangle bounds) {
1000       super.setBounds(bounds);
1001       if (myCloseRec.getParent() == null && getParent() != null) {
1002         myLayeredPane.add(myCloseRec, JLayeredPane.DRAG_LAYER);
1003       }
1004
1005       if (isVisible() && myCloseRec.isVisible()) {
1006         Rectangle lpBounds = SwingUtilities.convertRectangle(getParent(), bounds, myLayeredPane);
1007         lpBounds = myPosition.getPointlessContentRec(lpBounds, myBalloon.getPointerLength(myPosition));
1008
1009         int iconWidth = myBalloon.myCloseButton.getIconWidth();
1010         int iconHeight = myBalloon.myCloseButton.getIconHeight();
1011         Rectangle r = new Rectangle(lpBounds.x + lpBounds.width - iconWidth + (int)(iconWidth * 0.3), lpBounds.y - (int)(iconHeight * 0.3), iconWidth, iconHeight);
1012
1013
1014         myCloseRec.setBounds(r);
1015       }
1016
1017     }
1018
1019     public void repaintButton() {
1020       myCloseRec.repaint();
1021     }
1022   }
1023
1024   private static class Shaper {
1025     private final GeneralPath myPath = new GeneralPath();
1026
1027     Rectangle myBounds;
1028     private final int myTargetSide;
1029     private final BalloonImpl myBalloon;
1030
1031     public Shaper(BalloonImpl balloon, Rectangle bounds, Point targetPoint, int targetSide) {
1032       myBalloon = balloon;
1033       myBounds = bounds;
1034       myTargetSide = targetSide;
1035       start(targetPoint);
1036     }
1037
1038     private void start(Point start) {
1039       myPath.moveTo(start.x, start.y);
1040     }
1041
1042     public Shaper roundUpRight() {
1043       myPath.quadTo(getCurrent().x, getCurrent().y - myBalloon.getArc(), getCurrent().x + myBalloon.getArc(),
1044                     getCurrent().y - myBalloon.getArc());
1045       return this;
1046     }
1047
1048     public Shaper roundRightDown() {
1049       myPath.quadTo(getCurrent().x + myBalloon.getArc(), getCurrent().y, getCurrent().x + myBalloon.getArc(),
1050                     getCurrent().y + myBalloon.getArc());
1051       return this;
1052     }
1053
1054     public Shaper roundLeftUp() {
1055       myPath.quadTo(getCurrent().x - myBalloon.getArc(), getCurrent().y, getCurrent().x - myBalloon.getArc(),
1056                     getCurrent().y - myBalloon.getArc());
1057       return this;
1058     }
1059
1060     public Shaper roundLeftDown() {
1061       myPath.quadTo(getCurrent().x, getCurrent().y + myBalloon.getArc(), getCurrent().x - myBalloon.getArc(),
1062                     getCurrent().y + myBalloon.getArc());
1063       return this;
1064     }
1065
1066     public Point getCurrent() {
1067       return new Point((int)myPath.getCurrentPoint().getX(), (int)myPath.getCurrentPoint().getY());
1068     }
1069
1070     public Shaper line(final int deltaX, final int deltaY) {
1071       myPath.lineTo(getCurrent().x + deltaX, getCurrent().y + deltaY);
1072       return this;
1073     }
1074
1075     public Shaper lineTo(final int x, final int y) {
1076       myPath.lineTo(x, y);
1077       return this;
1078     }
1079
1080
1081     private int getTargetDelta(int effectiveSide) {
1082       return effectiveSide == myTargetSide ? myBalloon.getPointerLength(myBalloon.myPosition) : 0;
1083     }
1084
1085     public Shaper toRightCurve() {
1086       myPath.lineTo((int)myBounds.getMaxX() - myBalloon.getArc() - getTargetDelta(SwingUtilities.RIGHT) - 1, getCurrent().y);
1087       return this;
1088     }
1089
1090     public Shaper toBottomCurve() {
1091       myPath.lineTo(getCurrent().x, (int)myBounds.getMaxY() - myBalloon.getArc() - getTargetDelta(SwingUtilities.BOTTOM) - 1);
1092       return this;
1093     }
1094
1095     public Shaper toLeftCurve() {
1096       myPath.lineTo((int)myBounds.getX() + myBalloon.getArc() + getTargetDelta(SwingUtilities.LEFT), getCurrent().y);
1097       return this;
1098     }
1099
1100     public Shaper toTopCurve() {
1101       myPath.lineTo(getCurrent().x, (int)myBounds.getY() + myBalloon.getArc() + getTargetDelta(SwingUtilities.TOP));
1102       return this;
1103     }
1104
1105     public void close() {
1106       myPath.closePath();
1107     }
1108
1109     public Shape getShape() {
1110       return myPath;
1111     }
1112   }
1113
1114   public static void main(String[] args) {
1115     IconLoader.activate();
1116
1117     final JFrame frame = new JFrame();
1118     frame.getContentPane().setLayout(new BorderLayout());
1119     final JPanel content = new JPanel(new BorderLayout());
1120     frame.getContentPane().add(content, BorderLayout.CENTER);
1121
1122
1123     final JTree tree = new Tree();
1124     content.add(tree);
1125
1126
1127     final Ref<BalloonImpl> balloon = new Ref<BalloonImpl>();
1128
1129     tree.addMouseListener(new MouseAdapter() {
1130       @Override
1131       public void mousePressed(final MouseEvent e) {
1132         if (balloon.get() != null && balloon.get().isVisible()) {
1133           balloon.get().dispose();
1134         }
1135         else {
1136           //JLabel pane1 = new JLabel("Hello, world!");
1137           //JLabel pane2 = new JLabel("Hello, again");
1138           //JPanel pane = new JPanel(new BorderLayout());
1139           //pane.add(pane1, BorderLayout.CENTER);
1140           //pane.add(pane2, BorderLayout.SOUTH);
1141
1142           //pane.setBorder(new LineBorder(Color.blue));
1143
1144           balloon.set(new BalloonImpl(new JLabel("FUCK"), Color.black, MessageType.ERROR.getPopupBackground(), true, true, true, true, 0, true, null, false, 500, 5));
1145           balloon.get().setShowPointer(true);
1146
1147           if (e.isShiftDown()) {
1148             balloon.get().show(new RelativePoint(e), BalloonImpl.ABOVE);
1149           }
1150           else if (e.isAltDown()) {
1151             balloon.get().show(new RelativePoint(e), BalloonImpl.BELOW);
1152           }
1153           else if (e.isMetaDown()) {
1154             balloon.get().show(new RelativePoint(e), BalloonImpl.AT_LEFT);
1155           }
1156           else {
1157             balloon.get().show(new RelativePoint(e), BalloonImpl.AT_RIGHT);
1158           }
1159         }
1160       }
1161     });
1162
1163     tree.addMouseMotionListener(new MouseMotionAdapter() {
1164       @Override
1165       public void mouseMoved(MouseEvent e) {
1166         System.out.println(e.getPoint());
1167       }
1168     });
1169
1170     frame.setBounds(300, 300, 300, 300);
1171     frame.show();
1172   }
1173
1174   @Override
1175   public boolean wasFadedIn() {
1176     return myFadedIn;
1177   }
1178
1179   @Override
1180   public boolean wasFadedOut() {
1181     return myFadedOut;
1182   }
1183 }