Merge branch 'master' of git@git.labs.intellij.net:idea/community
[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     Position originalPreferred = position;
245
246     JRootPane root = null;
247     JDialog dialog = IJSwingUtilities.findParentOfType(tracker.getComponent(), JDialog.class);
248     if (dialog != null) {
249       root = dialog.getRootPane();
250     } else {
251       JFrame frame = IJSwingUtilities.findParentOfType(tracker.getComponent(), JFrame.class);
252       if (frame != null) {
253         root = frame.getRootPane();
254       } else {
255         assert false;
256       }
257     }
258
259     myVisible = true;
260
261     myLayeredPane = root.getLayeredPane();
262     myPosition = position;
263
264     myLayeredPane.addComponentListener(myComponentListener);
265
266     myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutshift);
267
268
269     if (myShowPointer) {
270       Rectangle rec = getRecForPosition(myPosition, true);
271
272       if (!myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc(), getNormalInset())) {
273         rec = getRecForPosition(myPosition, false);
274
275         Rectangle lp = new Rectangle(new Point(myContainerInsets.left, myContainerInsets.top), myLayeredPane.getSize());
276         lp.width -= myContainerInsets.right;
277         lp.height -= myContainerInsets.bottom;
278
279         if (!lp.contains(rec)) {
280           Rectangle2D currentSquare = lp.createIntersection(rec);
281
282           double maxSquare = currentSquare.getWidth() * currentSquare.getHeight();
283           Position targetPosition = myPosition;
284
285           for (Position eachPosition : myPosition.getOtherPositions()) {
286             Rectangle2D eachIntersection = lp.createIntersection(getRecForPosition(eachPosition, false));
287             double eachSquare = eachIntersection.getWidth() * eachIntersection.getHeight();
288             if (maxSquare < eachSquare) {
289               maxSquare = eachSquare;
290               targetPosition = eachPosition;
291             }
292           }
293
294           myPosition = targetPosition;
295         }
296       }
297     }
298
299     if (myPosition != originalPreferred) {
300       myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutshift);
301     }
302
303     createComponent();
304
305     myComp.validate();
306
307     Rectangle rec = myComp.getBounds();
308
309     if (myShowPointer && !myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc(), getNormalInset())) {
310       myShowPointer = false;
311       myComp.removeAll();
312       myLayeredPane.remove(myComp);
313
314       myForcedBounds = rec;
315       createComponent();
316     }
317
318     for (JBPopupListener each : myListeners) {
319       each.beforeShown(new LightweightWindowEvent(this));
320     }
321
322     runAnimation(true, myLayeredPane);
323
324     myLayeredPane.revalidate();
325     myLayeredPane.repaint();
326
327
328     Toolkit.getDefaultToolkit().addAWTEventListener(myAwtActivityListener, MouseEvent.MOUSE_EVENT_MASK |
329                                                                            MouseEvent.MOUSE_MOTION_EVENT_MASK |
330                                                                            KeyEvent.KEY_EVENT_MASK);
331   }
332
333   private Rectangle getRecForPosition(Position position, boolean adjust) {
334     Dimension size = getContentSizeFor(position);
335
336     Rectangle rec = new Rectangle(new Point(0, 0), size);
337
338     position.setRecToRelativePosition(rec, myTargetPoint);
339
340     if (adjust) {
341       rec = myPosition
342         .getUpdatedBounds(myLayeredPane.getSize(), myForcedBounds, rec.getSize(), myShowPointer, myTargetPoint, myContainerInsets, myCalloutshift);
343     }
344
345     return rec;
346   }
347
348   private Dimension getContentSizeFor(Position position) {
349     Insets insets = position.createBorder(this).getBorderInsets();
350     if (insets == null) {
351       insets = new Insets(0, 0, 0, 0);
352     }
353
354     Dimension size = myContent.getPreferredSize();
355     size.width += insets.left + insets.right;
356     size.height += insets.top + insets.bottom;
357
358     return size;
359   }
360
361   private void createComponent() {
362     myComp = new MyComponent(myContent, this, myShowPointer
363                                ? myPosition.createBorder(this)
364                                : getPointlessBorder());
365
366
367     myComp.clear();
368     myComp.myAlpha = 0f;
369
370
371     myLayeredPane.add(myComp, JLayeredPane.POPUP_LAYER);
372     myPosition.updateBounds(this);
373   }
374
375
376   private EmptyBorder getPointlessBorder() {
377     return new EmptyBorder(getNormalInset(), getNormalInset(), getNormalInset(), getNormalInset());
378   }
379
380   public void revalidate(PositionTracker<Balloon> tracker) {
381     RelativePoint newPosition = tracker.recalculateLocation(this);
382
383     if (newPosition != null) {
384       myTargetPoint = myPosition.getShiftedPoint(newPosition.getPoint(myLayeredPane), myCalloutshift);
385       myPosition.updateBounds(this);
386     }
387   }
388
389   public void show(JLayeredPane pane) {
390     show(pane, null);
391   }
392
393   public void show(JLayeredPane pane, @Nullable Rectangle bounds) {
394     if (bounds != null) {
395       myForcedBounds = bounds;
396     }
397     show(new RelativePoint(pane, new Point(0, 0)), Balloon.Position.above);
398   }
399
400
401   private void runAnimation(boolean forward, final JLayeredPane layeredPane) {
402     if (myAnimator != null) {
403       Disposer.dispose(myAnimator);
404     }
405     myAnimator = new Animator("Balloon", 10, myAnimationCycle, false, 0, 1, forward) {
406       public void paintNow(final float frame, final float totalFrames, final float cycle) {
407         if (myComp.getParent() == null) return;
408         myComp.setAlpha(frame / totalFrames);
409       }
410
411       @Override
412       protected void paintCycleEnd() {
413         if (myComp.getParent() == null) return;
414
415         if (isForward()) {
416           myComp.clear();
417           myComp.repaint();
418
419           myFadedIn = true;
420
421           startFadeoutTimer();
422         }
423         else {
424           layeredPane.remove(myComp);
425           layeredPane.revalidate();
426           layeredPane.repaint();
427         }
428         Disposer.dispose(this);
429       }
430
431       @Override
432       public void dispose() {
433         super.dispose();
434         myAnimator = null;
435       }
436     };
437
438     myAnimator.setTakInitialDelay(false);
439     myAnimator.resume();
440   }
441
442   private void startFadeoutTimer() {
443     if (myFadeoutTime > 0) {
444       Alarm fadeoutAlarm = new Alarm(this);
445       fadeoutAlarm.addRequest(new Runnable() {
446         public void run() {
447           hide();
448         }
449       }, (int)myFadeoutTime, null);
450     }
451   }
452
453
454   int getArc() {
455     return 3;
456   }
457
458   int getPointerWidth(Position position) {
459     return position.isTopBottomPointer() ? 14 : 11;
460   }
461
462   int getNormalInset() {
463     return 3;
464   }
465
466   int getPointerLength(Position position) {
467     return position.isTopBottomPointer() ? 10 : 8;
468   }
469
470   public void hide() {
471     Disposer.dispose(this);
472
473
474     for (JBPopupListener each : myListeners) {
475       each.onClosed(new LightweightWindowEvent(this));
476     }
477
478     myFadedOut = true;
479   }
480
481   public void addListener(JBPopupListener listener) {
482     myListeners.add(listener);
483   }
484
485   public void dispose() {
486     if (myDisposed) return;
487
488     Disposer.dispose(this);
489
490     myDisposed = true;
491
492     Toolkit.getDefaultToolkit().removeAWTEventListener(myAwtActivityListener);
493     if (myLayeredPane != null) {
494       myLayeredPane.removeComponentListener(myComponentListener);
495       runAnimation(false, myLayeredPane);
496     }
497
498
499     myVisible = false;
500
501     onDisposed();
502   }
503
504   protected void onDisposed() {
505
506   }
507
508   public boolean isVisible() {
509     return myVisible;
510   }
511
512   public void setShowPointer(final boolean show) {
513     myShowPointer = show;
514   }
515
516   public Icon getCloseButton() {
517     return myCloseButton;
518   }
519
520   public void setBounds(Rectangle bounds) {
521     myForcedBounds = bounds;
522     if (myPosition != null) {
523       myPosition.updateBounds(this);
524     }
525   }
526
527   public Dimension getPreferredSize() {
528     if (myComp != null) {
529       return myComp.getPreferredSize();
530     } else {
531       if (myDefaultPrefSize == null) {
532         final EmptyBorder border = getPointlessBorder();
533         final MyComponent c = new MyComponent(myContent, this, border);
534         myDefaultPrefSize = c.getPreferredSize();
535       }
536       return myDefaultPrefSize;
537     }
538   }
539
540   public abstract static class Position {
541
542     abstract EmptyBorder createBorder(final BalloonImpl balloon);
543
544
545     abstract void setRecToRelativePosition(Rectangle rec, Point targetPoint);
546
547
548     public void updateBounds(final BalloonImpl balloon) {
549       balloon.myComp._setBounds(getUpdatedBounds(balloon.myLayeredPane.getSize(),
550                                                  balloon.myForcedBounds,
551                                                  balloon.myComp.getPreferredSize(),
552                                                  balloon.myShowPointer,
553                                                  balloon.myTargetPoint,
554                                                  balloon.myContainerInsets,
555                                                  balloon.myCalloutshift));
556     }
557
558     public Rectangle getUpdatedBounds(Dimension layeredPaneSize,
559                                       Rectangle forcedBounds,
560                                       Dimension preferredSize,
561                                       boolean showPointer,
562                                       Point point, Insets containerInsets, int calloutShift) {
563
564       Rectangle bounds = forcedBounds;
565
566       if (bounds == null) {
567         Point location = showPointer
568                          ? getLocation(layeredPaneSize, point, preferredSize)
569                          : new Point(point.x - preferredSize.width / 2, point.y - preferredSize.height / 2);
570         bounds = new Rectangle(location.x, location.y, preferredSize.width, preferredSize.height);
571
572         ScreenUtil.moveToFit(bounds, new Rectangle(0, 0, layeredPaneSize.width, layeredPaneSize.height), containerInsets);
573       }
574
575       return bounds;
576     }
577
578     abstract Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize);
579
580     void paintComponent(BalloonImpl balloon, final Rectangle bounds, final Graphics2D g, Point pointTarget) {
581       final GraphicsConfig cfg = new GraphicsConfig(g);
582       cfg.setAntialiasing(true);
583
584       Shape shape;
585       if (balloon.myShowPointer) {
586         shape = getPointingShape(bounds, g, pointTarget, balloon);
587       }
588       else {
589         shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, balloon.getArc(), balloon.getArc());
590       }
591
592       g.setColor(balloon.myFillColor);
593       g.fill(shape);
594       g.setColor(balloon.myBorderColor);
595       g.draw(shape);
596       cfg.restore();
597     }
598
599     protected abstract Shape getPointingShape(final Rectangle bounds,
600                                               final Graphics2D g,
601                                               final Point pointTarget,
602                                               final BalloonImpl balloon);
603
604     public boolean isOkToHavePointer(Point targetPoint, Rectangle bounds, int pointerLength, int pointerWidth, int arc, int normalInset) {
605       if (bounds.x < targetPoint.x && bounds.x + bounds.width > targetPoint.x && bounds.y < targetPoint.y && bounds.y + bounds.height < targetPoint.y) return false;
606
607       Rectangle pointless = getPointlessContentRec(bounds, pointerLength);
608
609       int size = getDistanceToTarget(pointless, targetPoint);
610       if (size < pointerLength) return false;
611
612       Range<Integer> balloonRange;
613       Range<Integer> pointerRange;
614       if (isTopBottomPointer()) {
615         balloonRange = new Range<Integer>(bounds.x + arc, bounds.x + bounds.width - arc * 2);
616         pointerRange = new Range<Integer>(targetPoint.x - pointerWidth / 2, targetPoint.x + pointerWidth / 2);
617       } else {
618         balloonRange = new Range<Integer>(bounds.y + arc, bounds.y + bounds.height - arc * 2);
619         pointerRange = new Range<Integer>(targetPoint.y - pointerWidth / 2, targetPoint.y + pointerWidth / 2);
620       }
621
622       return balloonRange.isWithin(pointerRange.getFrom()) && balloonRange.isWithin(pointerRange.getTo());
623     }
624
625     protected abstract int getDistanceToTarget(Rectangle rectangle, Point targetPoint);
626
627     protected boolean isTopBottomPointer() {
628       return this instanceof Below || this instanceof Above;
629     }
630
631     protected abstract Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength);
632
633     public Set<Position> getOtherPositions() {
634       HashSet<Position> all = new HashSet<Position>();
635       all.add(BELOW);
636       all.add(ABOVE);
637       all.add(AT_RIGHT);
638       all.add(AT_LEFT);
639
640       all.remove(this);
641
642       return all;
643     }
644
645     public abstract Point getShiftedPoint(Point targetPoint, int shift);
646   }
647
648   public static final Position BELOW = new Below();
649   public static final Position ABOVE = new Above();
650   public static final Position AT_RIGHT = new AtRight();
651   public static final Position AT_LEFT = new AtLeft();
652
653
654   private static class Below extends Position {
655
656
657     @Override
658     public Point getShiftedPoint(Point targetPoint, int shift) {
659       return new Point(targetPoint.x, targetPoint.y + shift);
660     }
661
662     @Override
663     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
664       return rectangle.y - targetPoint.y;
665     }
666
667     @Override
668     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
669       return new Rectangle(bounds.x, bounds.y + pointerLength, bounds.width, bounds.height - pointerLength);
670     }
671
672     EmptyBorder createBorder(final BalloonImpl balloon) {
673       return new EmptyBorder(balloon.getPointerLength(this) + balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset());
674     }
675
676     @Override
677     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
678       rec.setLocation(new Point(targetPoint.x - rec.width / 2, targetPoint.y));
679     }
680
681     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
682       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
683       return new Point(center.x, targetPoint.y);
684     }
685
686     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
687       bounds.y += balloon.getPointerLength(this);
688       bounds.height -= balloon.getPointerLength(this) - 1;
689     }
690
691     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
692       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.TOP);
693       shaper.line(balloon.getPointerWidth(this) / 2, balloon.getPointerLength(this)).toRightCurve().roundRightDown().toBottomCurve().roundLeftDown()
694         .toLeftCurve().roundLeftUp().toTopCurve().roundUpRight()
695         .lineTo(pointTarget.x - balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y);
696       shaper.close();
697
698       return shaper.getShape();
699     }
700
701     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
702       bounds.y += balloon.getPointerLength(this);
703       bounds.height += balloon.getPointerLength(this);
704       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
705     }
706
707   }
708
709   private static class Above extends Position {
710
711     @Override
712     public Point getShiftedPoint(Point targetPoint, int shift) {
713       return new Point(targetPoint.x, targetPoint.y - shift);
714     }
715
716     @Override
717     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
718       return targetPoint.y - (int)rectangle.getMaxY();
719     }
720
721     @Override
722     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
723       return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height - pointerLength);
724     }
725
726     EmptyBorder createBorder(final BalloonImpl balloon) {
727       return new EmptyBorder(balloon.getNormalInset(),
728                              balloon.getNormalInset(),
729                              balloon.getPointerLength(this),
730                              balloon.getNormalInset());
731     }
732
733     @Override
734     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
735       rec.setLocation(targetPoint.x - rec.width / 2, targetPoint.y - rec.height);
736     }
737
738     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
739       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
740       return new Point(center.x, targetPoint.y - balloonSize.height);
741     }
742
743     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
744       bounds.height -= balloon.getPointerLength(this) - 1;
745     }
746
747     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
748       bounds.y -= balloon.getPointerLength(this);
749       bounds.height -= balloon.getPointerLength(this);
750       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
751     }
752
753     @Override
754     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
755       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.BOTTOM);
756       shaper.line(-balloon.getPointerWidth(this) / 2, -balloon.getPointerLength(this) + 1);
757       shaper.toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown().toBottomCurve().line(0, 2)
758         .roundLeftDown().lineTo(pointTarget.x + balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y)
759         .close();
760
761
762       return shaper.getShape();
763     }
764   }
765
766   private static class AtRight extends Position {
767
768     @Override
769     public Point getShiftedPoint(Point targetPoint, int shift) {
770       return new Point(targetPoint.x + shift, targetPoint.y);
771     }
772
773     @Override
774     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
775       return rectangle.x - targetPoint.x;
776     }
777
778     @Override
779     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
780       return new Rectangle(bounds.x + pointerLength, bounds.y, bounds.width - pointerLength, bounds.height);
781     }
782
783     EmptyBorder createBorder(final BalloonImpl balloon) {
784       return new EmptyBorder(balloon.getNormalInset(), balloon.getPointerLength(this) + balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset());
785     }
786
787     @Override
788     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
789       rec.setLocation(targetPoint.x, targetPoint.y - rec.height / 2);
790     }
791
792     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
793       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
794       return new Point(targetPoint.x, center.y);
795     }
796
797     @Override
798     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
799       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.LEFT);
800       shaper.line(balloon.getPointerLength(this), -balloon.getPointerWidth(this) / 2).toTopCurve().roundUpRight().toRightCurve().roundRightDown()
801         .toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp()
802         .lineTo(shaper.getCurrent().x, pointTarget.y + balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
803
804       return shaper.getShape();
805     }
806
807     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
808       bounds.x += balloon.getPointerLength(this);
809       bounds.width -= balloon.getPointerLength(this);
810     }
811
812     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
813       bounds.x += balloon.getPointerLength(this);
814       bounds.width -= balloon.getPointerLength(this);
815       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
816     }
817   }
818
819   private static class AtLeft extends Position {
820
821     @Override
822     public Point getShiftedPoint(Point targetPoint, int shift) {
823       return new Point(targetPoint.x - shift, targetPoint.y);
824     }
825
826     @Override
827     protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
828       return targetPoint.x - (int)rectangle.getMaxX();
829     }
830
831     @Override
832     protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
833       return new Rectangle(bounds.x, bounds.y, bounds.width - pointerLength, bounds.height);
834     }
835
836     EmptyBorder createBorder(final BalloonImpl balloon) {
837       return new EmptyBorder(balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset(), balloon.getPointerLength(this) + balloon.getNormalInset());
838     }
839
840     @Override
841     void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
842       rec.setLocation(targetPoint.x - rec.width, targetPoint.y - rec.height / 2);
843     }
844
845     Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) {
846       final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize);
847       return new Point(targetPoint.x - balloonSize.width, center.y);
848     }
849
850     protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
851       bounds.width -= balloon.getPointerLength(this);
852     }
853
854     @Override
855     protected Shape getPointingShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
856       final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingUtilities.RIGHT);
857       shaper.line(-balloon.getPointerLength(this), balloon.getPointerWidth(this) / 2);
858       shaper.toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown()
859         .lineTo(shaper.getCurrent().x, pointTarget.y - balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close();
860       return shaper.getShape();
861     }
862
863     protected Shape getShape(final Rectangle bounds, final Graphics2D g, final Point pointTarget, final BalloonImpl balloon) {
864       bounds.width -= balloon.getPointerLength(this);
865       return new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, balloon.getArc(), balloon.getArc());
866     }
867   }
868
869   private class CloseButton extends NonOpaquePanel {
870
871     private BaseButtonBehavior myButton;
872
873     private CloseButton() {
874       myButton = new BaseButtonBehavior(this, TimedDeadzone.NULL) {
875         protected void execute(MouseEvent e) {
876           //noinspection SSBasedInspection
877           SwingUtilities.invokeLater(new Runnable() {
878             public void run() {
879               BalloonImpl.this.hide();
880             }
881           });
882         }
883       };
884
885     }
886
887     @Override
888     protected void paintComponent(Graphics g) {
889       super.paintComponent(g);
890
891       if (!myEnableCloseButton) return;
892
893       if (getWidth() > 0 && myLastMoveWasInsideBalloon) {
894         final boolean pressed = myButton.isPressedByMouse();
895         getCloseButton().paintIcon(this, g, (pressed ? 1 : 0), (pressed ? 1 : 0));
896       }
897     }
898   }
899
900   private class MyComponent extends JPanel {
901
902     private BufferedImage myImage;
903     private float myAlpha;
904     private final BalloonImpl myBalloon;
905
906     private final Wrapper myContent;
907
908     private MyComponent(JComponent content, BalloonImpl balloon, EmptyBorder shapeBorder) {
909       setOpaque(false);
910       setLayout(null);
911       myBalloon = balloon;
912
913       myContent = new Wrapper(content);
914       myContent.setBorder(shapeBorder);
915       myContent.setOpaque(false);
916
917       add(myContent);
918
919       myCloseRec = new CloseButton();
920     }
921
922     public void clear() {
923       myImage = null;
924       myAlpha = -1;
925     }
926
927     @Override
928     public void doLayout() {
929       Insets insets = getInsets();
930       if (insets == null) {
931         insets = new Insets(0, 0, 0, 0);
932       }
933
934       myContent.setBounds(insets.left, insets.top, getWidth() - insets.left - insets.right, getHeight() - insets.top - insets.bottom);
935     }
936
937     @Override
938     public Dimension getPreferredSize() {
939       return addInsets(myContent.getPreferredSize());
940     }
941
942     @Override
943     public Dimension getMinimumSize() {
944       return addInsets(myContent.getMinimumSize());
945     }
946
947     private Dimension addInsets(Dimension size) {
948       final Insets insets = getInsets();
949       if (insets != null) {
950         size.width += (insets.left + insets.right);
951         size.height += (insets.top + insets.bottom);
952       }
953
954       return size;
955     }
956
957     @Override
958     protected void paintComponent(final Graphics g) {
959       super.paintComponent(g);
960
961       final Graphics2D g2d = (Graphics2D)g;
962
963       final Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
964
965       Rectangle shapeBounds = myContent.getBounds();
966
967       if (myImage == null && myAlpha != -1) {
968         myImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
969         myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)myImage.getGraphics(), pointTarget);
970       }
971
972       if (myImage != null && myAlpha != -1) {
973         g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, myAlpha));
974
975         g2d.drawImage(myImage, 0, 0, null);
976       }
977       else {
978         myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)g, pointTarget);
979       }
980     }
981
982
983     @Override
984     public void removeNotify() {
985       super.removeNotify();
986
987       if (myLayeredPane != null) {
988         final JLayeredPane pane = myLayeredPane;
989         SwingUtilities.invokeLater(new Runnable() {
990           @Override
991           public void run() {
992             pane.remove(myCloseRec);
993           }
994         });
995       }
996     }
997
998     public void setAlpha(float alpha) {
999       myAlpha = alpha;
1000       paintImmediately(0, 0, getWidth(), getHeight());
1001     }
1002
1003     public void _setBounds(Rectangle bounds) {
1004       super.setBounds(bounds);
1005       if (myCloseRec.getParent() == null && getParent() != null) {
1006         myLayeredPane.add(myCloseRec, JLayeredPane.DRAG_LAYER);
1007       }
1008
1009       if (isVisible() && myCloseRec.isVisible()) {
1010         Rectangle lpBounds = SwingUtilities.convertRectangle(getParent(), bounds, myLayeredPane);
1011         lpBounds = myPosition.getPointlessContentRec(lpBounds, myBalloon.getPointerLength(myPosition));
1012
1013         int iconWidth = myBalloon.myCloseButton.getIconWidth();
1014         int iconHeight = myBalloon.myCloseButton.getIconHeight();
1015         Rectangle r = new Rectangle(lpBounds.x + lpBounds.width - iconWidth + (int)(iconWidth * 0.3), lpBounds.y - (int)(iconHeight * 0.3), iconWidth, iconHeight);
1016
1017
1018         myCloseRec.setBounds(r);
1019       }
1020
1021     }
1022
1023     public void repaintButton() {
1024       myCloseRec.repaint();
1025     }
1026   }
1027
1028   private static class Shaper {
1029     private final GeneralPath myPath = new GeneralPath();
1030
1031     Rectangle myBounds;
1032     private final int myTargetSide;
1033     private final BalloonImpl myBalloon;
1034
1035     public Shaper(BalloonImpl balloon, Rectangle bounds, Point targetPoint, int targetSide) {
1036       myBalloon = balloon;
1037       myBounds = bounds;
1038       myTargetSide = targetSide;
1039       start(targetPoint);
1040     }
1041
1042     private void start(Point start) {
1043       myPath.moveTo(start.x, start.y);
1044     }
1045
1046     public Shaper roundUpRight() {
1047       myPath.quadTo(getCurrent().x, getCurrent().y - myBalloon.getArc(), getCurrent().x + myBalloon.getArc(),
1048                     getCurrent().y - myBalloon.getArc());
1049       return this;
1050     }
1051
1052     public Shaper roundRightDown() {
1053       myPath.quadTo(getCurrent().x + myBalloon.getArc(), getCurrent().y, getCurrent().x + myBalloon.getArc(),
1054                     getCurrent().y + myBalloon.getArc());
1055       return this;
1056     }
1057
1058     public Shaper roundLeftUp() {
1059       myPath.quadTo(getCurrent().x - myBalloon.getArc(), getCurrent().y, getCurrent().x - myBalloon.getArc(),
1060                     getCurrent().y - myBalloon.getArc());
1061       return this;
1062     }
1063
1064     public Shaper roundLeftDown() {
1065       myPath.quadTo(getCurrent().x, getCurrent().y + myBalloon.getArc(), getCurrent().x - myBalloon.getArc(),
1066                     getCurrent().y + myBalloon.getArc());
1067       return this;
1068     }
1069
1070     public Point getCurrent() {
1071       return new Point((int)myPath.getCurrentPoint().getX(), (int)myPath.getCurrentPoint().getY());
1072     }
1073
1074     public Shaper line(final int deltaX, final int deltaY) {
1075       myPath.lineTo(getCurrent().x + deltaX, getCurrent().y + deltaY);
1076       return this;
1077     }
1078
1079     public Shaper lineTo(final int x, final int y) {
1080       myPath.lineTo(x, y);
1081       return this;
1082     }
1083
1084
1085     private int getTargetDelta(int effectiveSide) {
1086       return effectiveSide == myTargetSide ? myBalloon.getPointerLength(myBalloon.myPosition) : 0;
1087     }
1088
1089     public Shaper toRightCurve() {
1090       myPath.lineTo((int)myBounds.getMaxX() - myBalloon.getArc() - getTargetDelta(SwingUtilities.RIGHT) - 1, getCurrent().y);
1091       return this;
1092     }
1093
1094     public Shaper toBottomCurve() {
1095       myPath.lineTo(getCurrent().x, (int)myBounds.getMaxY() - myBalloon.getArc() - getTargetDelta(SwingUtilities.BOTTOM) - 1);
1096       return this;
1097     }
1098
1099     public Shaper toLeftCurve() {
1100       myPath.lineTo((int)myBounds.getX() + myBalloon.getArc() + getTargetDelta(SwingUtilities.LEFT), getCurrent().y);
1101       return this;
1102     }
1103
1104     public Shaper toTopCurve() {
1105       myPath.lineTo(getCurrent().x, (int)myBounds.getY() + myBalloon.getArc() + getTargetDelta(SwingUtilities.TOP));
1106       return this;
1107     }
1108
1109     public void close() {
1110       myPath.closePath();
1111     }
1112
1113     public Shape getShape() {
1114       return myPath;
1115     }
1116   }
1117
1118   public static void main(String[] args) {
1119     IconLoader.activate();
1120
1121     final JFrame frame = new JFrame();
1122     frame.getContentPane().setLayout(new BorderLayout());
1123     final JPanel content = new JPanel(new BorderLayout());
1124     frame.getContentPane().add(content, BorderLayout.CENTER);
1125
1126
1127     final JTree tree = new Tree();
1128     content.add(tree);
1129
1130
1131     final Ref<BalloonImpl> balloon = new Ref<BalloonImpl>();
1132
1133     tree.addMouseListener(new MouseAdapter() {
1134       @Override
1135       public void mousePressed(final MouseEvent e) {
1136         if (balloon.get() != null && balloon.get().isVisible()) {
1137           balloon.get().dispose();
1138         }
1139         else {
1140           //JLabel pane1 = new JLabel("Hello, world!");
1141           //JLabel pane2 = new JLabel("Hello, again");
1142           //JPanel pane = new JPanel(new BorderLayout());
1143           //pane.add(pane1, BorderLayout.CENTER);
1144           //pane.add(pane2, BorderLayout.SOUTH);
1145
1146           //pane.setBorder(new LineBorder(Color.blue));
1147
1148           balloon.set(new BalloonImpl(new JLabel("FUCK"), Color.black, MessageType.ERROR.getPopupBackground(), true, true, true, true, 0, true, null, false, 500, 5));
1149           balloon.get().setShowPointer(true);
1150
1151           if (e.isShiftDown()) {
1152             balloon.get().show(new RelativePoint(e), BalloonImpl.ABOVE);
1153           }
1154           else if (e.isAltDown()) {
1155             balloon.get().show(new RelativePoint(e), BalloonImpl.BELOW);
1156           }
1157           else if (e.isMetaDown()) {
1158             balloon.get().show(new RelativePoint(e), BalloonImpl.AT_LEFT);
1159           }
1160           else {
1161             balloon.get().show(new RelativePoint(e), BalloonImpl.AT_RIGHT);
1162           }
1163         }
1164       }
1165     });
1166
1167     tree.addMouseMotionListener(new MouseMotionAdapter() {
1168       @Override
1169       public void mouseMoved(MouseEvent e) {
1170         System.out.println(e.getPoint());
1171       }
1172     });
1173
1174     frame.setBounds(300, 300, 300, 300);
1175     frame.show();
1176   }
1177
1178   @Override
1179   public boolean wasFadedIn() {
1180     return myFadedIn;
1181   }
1182
1183   @Override
1184   public boolean wasFadedOut() {
1185     return myFadedOut;
1186   }
1187 }