2 * Copyright 2000-2009 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.ui;
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;
39 import javax.swing.border.EmptyBorder;
40 import javax.swing.border.LineBorder;
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;
48 import java.util.concurrent.CopyOnWriteArraySet;
50 public class BalloonImpl implements Disposable, Balloon, LightweightWindow, PositionTracker.Client<Balloon> {
52 private MyComponent myComp;
53 private JLayeredPane myLayeredPane;
54 private Position myPosition;
55 private Point myTargetPoint;
56 private final boolean myHideOnFrameResize;
58 private final Color myBorderColor;
59 private final Color myFillColor;
61 private final Insets myContainerInsets = new Insets(2, 2, 2, 2);
63 private boolean myLastMoveWasInsideBalloon;
65 private Rectangle myForcedBounds;
67 private CloseButton myCloseRec;
69 private final AWTEventListener myAwtActivityListener = new AWTEventListener() {
70 public void eventDispatched(final AWTEvent event) {
72 (event.getID() == MouseEvent.MOUSE_PRESSED)) {
73 final MouseEvent me = (MouseEvent)event;
74 if (isInsideBalloon(me)) return;
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()));
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;
97 myComp.repaintButton();
101 if (event instanceof MouseEvent && UIUtil.isCloseClick((MouseEvent)event)) {
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;
115 private final long myFadeoutTime;
116 private Dimension myDefaultPrefSize;
117 private final ActionListener myClickHandler;
118 private final boolean myCloseOnClick;
120 private final CopyOnWriteArraySet<JBPopupListener> myListeners = new CopyOnWriteArraySet<JBPopupListener>();
121 private boolean myVisible;
122 private PositionTracker<Balloon> myTracker;
123 private int myAnimationCycle = 500;
125 private boolean myFadedIn;
126 private boolean myFadedOut;
127 private int myCalloutshift;
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;
134 final Point mouseEventPoint = me.getPoint();
135 SwingUtilities.convertPointToScreen(mouseEventPoint, me.getComponent());
137 if (!myComp.isShowing()) return false;
139 final Rectangle compRect = new Rectangle(myComp.getLocationOnScreen(), myComp.getSize());
140 if (compRect.contains(mouseEventPoint)) return true;
144 private final ComponentAdapter myComponentListener = new ComponentAdapter() {
145 public void componentResized(final ComponentEvent e) {
146 if (myHideOnFrameResize) {
151 private Animator myAnimator;
152 private boolean myShowPointer;
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");
161 public BalloonImpl(JComponent content,
167 boolean enableCloseButton,
169 boolean hideOnFrameResize,
170 ActionListener clickHandler,
171 boolean closeOnClick,
174 myBorderColor = borderColor;
175 myFillColor = fillColor;
177 myHideOnMouse = hideOnMouse;
178 myHideOnKey = hideOnKey;
179 myShowPointer = showPointer;
180 myEnableCloseButton = enableCloseButton;
181 myHideOnFrameResize = hideOnFrameResize;
182 myClickHandler = clickHandler;
183 myCloseOnClick = closeOnClick;
184 myCalloutshift = calloutShift;
186 myFadeoutTime = fadeoutTime;
187 myAnimationCycle = animationCycle;
190 public void show(final RelativePoint target, final Balloon.Position position) {
191 Position pos = BELOW;
210 public void show(PositionTracker<Balloon> tracker, Balloon.Position position) {
211 Position pos = BELOW;
231 private void show(RelativePoint target, Position position) {
232 show(new PositionTracker.Static<Balloon>(target), position);
235 private void show(PositionTracker<Balloon> tracker, Position position) {
236 if (isVisible()) return;
238 assert !myDisposed : "Balloon is already disposed";
239 assert tracker.getComponent().isShowing() : "Target component is not showing: " + tracker;
242 myTracker.init(this);
244 Position originalPreferred = position;
246 JRootPane root = null;
247 JDialog dialog = IJSwingUtilities.findParentOfType(tracker.getComponent(), JDialog.class);
248 if (dialog != null) {
249 root = dialog.getRootPane();
251 JFrame frame = IJSwingUtilities.findParentOfType(tracker.getComponent(), JFrame.class);
253 root = frame.getRootPane();
261 myLayeredPane = root.getLayeredPane();
262 myPosition = position;
264 myLayeredPane.addComponentListener(myComponentListener);
266 myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutshift);
270 Rectangle rec = getRecForPosition(myPosition, true);
272 if (!myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc(), getNormalInset())) {
273 rec = getRecForPosition(myPosition, false);
275 Rectangle lp = new Rectangle(new Point(myContainerInsets.left, myContainerInsets.top), myLayeredPane.getSize());
276 lp.width -= myContainerInsets.right;
277 lp.height -= myContainerInsets.bottom;
279 if (!lp.contains(rec)) {
280 Rectangle2D currentSquare = lp.createIntersection(rec);
282 double maxSquare = currentSquare.getWidth() * currentSquare.getHeight();
283 Position targetPosition = myPosition;
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;
294 myPosition = targetPosition;
299 if (myPosition != originalPreferred) {
300 myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutshift);
307 Rectangle rec = myComp.getBounds();
309 if (myShowPointer && !myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc(), getNormalInset())) {
310 myShowPointer = false;
312 myLayeredPane.remove(myComp);
314 myForcedBounds = rec;
318 for (JBPopupListener each : myListeners) {
319 each.beforeShown(new LightweightWindowEvent(this));
322 runAnimation(true, myLayeredPane);
324 myLayeredPane.revalidate();
325 myLayeredPane.repaint();
328 Toolkit.getDefaultToolkit().addAWTEventListener(myAwtActivityListener, MouseEvent.MOUSE_EVENT_MASK |
329 MouseEvent.MOUSE_MOTION_EVENT_MASK |
330 KeyEvent.KEY_EVENT_MASK);
333 private Rectangle getRecForPosition(Position position, boolean adjust) {
334 Dimension size = getContentSizeFor(position);
336 Rectangle rec = new Rectangle(new Point(0, 0), size);
338 position.setRecToRelativePosition(rec, myTargetPoint);
342 .getUpdatedBounds(myLayeredPane.getSize(), myForcedBounds, rec.getSize(), myShowPointer, myTargetPoint, myContainerInsets, myCalloutshift);
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);
354 Dimension size = myContent.getPreferredSize();
355 size.width += insets.left + insets.right;
356 size.height += insets.top + insets.bottom;
361 private void createComponent() {
362 myComp = new MyComponent(myContent, this, myShowPointer
363 ? myPosition.createBorder(this)
364 : getPointlessBorder());
371 myLayeredPane.add(myComp, JLayeredPane.POPUP_LAYER);
372 myPosition.updateBounds(this);
376 private EmptyBorder getPointlessBorder() {
377 return new EmptyBorder(getNormalInset(), getNormalInset(), getNormalInset(), getNormalInset());
380 public void revalidate(PositionTracker<Balloon> tracker) {
381 RelativePoint newPosition = tracker.recalculateLocation(this);
383 if (newPosition != null) {
384 myTargetPoint = myPosition.getShiftedPoint(newPosition.getPoint(myLayeredPane), myCalloutshift);
385 myPosition.updateBounds(this);
389 public void show(JLayeredPane pane) {
393 public void show(JLayeredPane pane, @Nullable Rectangle bounds) {
394 if (bounds != null) {
395 myForcedBounds = bounds;
397 show(new RelativePoint(pane, new Point(0, 0)), Balloon.Position.above);
401 private void runAnimation(boolean forward, final JLayeredPane layeredPane) {
402 if (myAnimator != null) {
403 Disposer.dispose(myAnimator);
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);
412 protected void paintCycleEnd() {
413 if (myComp.getParent() == null) return;
424 layeredPane.remove(myComp);
425 layeredPane.revalidate();
426 layeredPane.repaint();
428 Disposer.dispose(this);
432 public void dispose() {
438 myAnimator.setTakInitialDelay(false);
442 private void startFadeoutTimer() {
443 if (myFadeoutTime > 0) {
444 Alarm fadeoutAlarm = new Alarm(this);
445 fadeoutAlarm.addRequest(new Runnable() {
449 }, (int)myFadeoutTime, null);
458 int getPointerWidth(Position position) {
459 return position.isTopBottomPointer() ? 14 : 11;
462 int getNormalInset() {
466 int getPointerLength(Position position) {
467 return position.isTopBottomPointer() ? 10 : 8;
471 Disposer.dispose(this);
474 for (JBPopupListener each : myListeners) {
475 each.onClosed(new LightweightWindowEvent(this));
481 public void addListener(JBPopupListener listener) {
482 myListeners.add(listener);
485 public void dispose() {
486 if (myDisposed) return;
488 Disposer.dispose(this);
492 Toolkit.getDefaultToolkit().removeAWTEventListener(myAwtActivityListener);
493 if (myLayeredPane != null) {
494 myLayeredPane.removeComponentListener(myComponentListener);
495 runAnimation(false, myLayeredPane);
504 protected void onDisposed() {
508 public boolean isVisible() {
512 public void setShowPointer(final boolean show) {
513 myShowPointer = show;
516 public Icon getCloseButton() {
517 return myCloseButton;
520 public void setBounds(Rectangle bounds) {
521 myForcedBounds = bounds;
522 if (myPosition != null) {
523 myPosition.updateBounds(this);
527 public Dimension getPreferredSize() {
528 if (myComp != null) {
529 return myComp.getPreferredSize();
531 if (myDefaultPrefSize == null) {
532 final EmptyBorder border = getPointlessBorder();
533 final MyComponent c = new MyComponent(myContent, this, border);
534 myDefaultPrefSize = c.getPreferredSize();
536 return myDefaultPrefSize;
540 public abstract static class Position {
542 abstract EmptyBorder createBorder(final BalloonImpl balloon);
545 abstract void setRecToRelativePosition(Rectangle rec, Point targetPoint);
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));
558 public Rectangle getUpdatedBounds(Dimension layeredPaneSize,
559 Rectangle forcedBounds,
560 Dimension preferredSize,
562 Point point, Insets containerInsets, int calloutShift) {
564 Rectangle bounds = forcedBounds;
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);
572 ScreenUtil.moveToFit(bounds, new Rectangle(0, 0, layeredPaneSize.width, layeredPaneSize.height), containerInsets);
578 abstract Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize);
580 void paintComponent(BalloonImpl balloon, final Rectangle bounds, final Graphics2D g, Point pointTarget) {
581 final GraphicsConfig cfg = new GraphicsConfig(g);
582 cfg.setAntialiasing(true);
585 if (balloon.myShowPointer) {
586 shape = getPointingShape(bounds, g, pointTarget, balloon);
589 shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, balloon.getArc(), balloon.getArc());
592 g.setColor(balloon.myFillColor);
594 g.setColor(balloon.myBorderColor);
599 protected abstract Shape getPointingShape(final Rectangle bounds,
601 final Point pointTarget,
602 final BalloonImpl balloon);
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;
607 Rectangle pointless = getPointlessContentRec(bounds, pointerLength);
609 int size = getDistanceToTarget(pointless, targetPoint);
610 if (size < pointerLength) return false;
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);
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);
622 return balloonRange.isWithin(pointerRange.getFrom()) && balloonRange.isWithin(pointerRange.getTo());
625 protected abstract int getDistanceToTarget(Rectangle rectangle, Point targetPoint);
627 protected boolean isTopBottomPointer() {
628 return this instanceof Below || this instanceof Above;
631 protected abstract Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength);
633 public Set<Position> getOtherPositions() {
634 HashSet<Position> all = new HashSet<Position>();
645 public abstract Point getShiftedPoint(Point targetPoint, int shift);
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();
654 private static class Below extends Position {
658 public Point getShiftedPoint(Point targetPoint, int shift) {
659 return new Point(targetPoint.x, targetPoint.y + shift);
663 protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
664 return rectangle.y - targetPoint.y;
668 protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
669 return new Rectangle(bounds.x, bounds.y + pointerLength, bounds.width, bounds.height - pointerLength);
672 EmptyBorder createBorder(final BalloonImpl balloon) {
673 return new EmptyBorder(balloon.getPointerLength(this) + balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset());
677 void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
678 rec.setLocation(new Point(targetPoint.x - rec.width / 2, targetPoint.y));
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);
686 protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
687 bounds.y += balloon.getPointerLength(this);
688 bounds.height -= balloon.getPointerLength(this) - 1;
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);
698 return shaper.getShape();
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());
709 private static class Above extends Position {
712 public Point getShiftedPoint(Point targetPoint, int shift) {
713 return new Point(targetPoint.x, targetPoint.y - shift);
717 protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
718 return targetPoint.y - (int)rectangle.getMaxY();
722 protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
723 return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height - pointerLength);
726 EmptyBorder createBorder(final BalloonImpl balloon) {
727 return new EmptyBorder(balloon.getNormalInset(),
728 balloon.getNormalInset(),
729 balloon.getPointerLength(this),
730 balloon.getNormalInset());
734 void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
735 rec.setLocation(targetPoint.x - rec.width / 2, targetPoint.y - rec.height);
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);
743 protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
744 bounds.height -= balloon.getPointerLength(this) - 1;
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());
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)
762 return shaper.getShape();
766 private static class AtRight extends Position {
769 public Point getShiftedPoint(Point targetPoint, int shift) {
770 return new Point(targetPoint.x + shift, targetPoint.y);
774 protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
775 return rectangle.x - targetPoint.x;
779 protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
780 return new Rectangle(bounds.x + pointerLength, bounds.y, bounds.width - pointerLength, bounds.height);
783 EmptyBorder createBorder(final BalloonImpl balloon) {
784 return new EmptyBorder(balloon.getNormalInset(), balloon.getPointerLength(this) + balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset());
788 void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
789 rec.setLocation(targetPoint.x, targetPoint.y - rec.height / 2);
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);
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();
804 return shaper.getShape();
807 protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
808 bounds.x += balloon.getPointerLength(this);
809 bounds.width -= balloon.getPointerLength(this);
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());
819 private static class AtLeft extends Position {
822 public Point getShiftedPoint(Point targetPoint, int shift) {
823 return new Point(targetPoint.x - shift, targetPoint.y);
827 protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) {
828 return targetPoint.x - (int)rectangle.getMaxX();
832 protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) {
833 return new Rectangle(bounds.x, bounds.y, bounds.width - pointerLength, bounds.height);
836 EmptyBorder createBorder(final BalloonImpl balloon) {
837 return new EmptyBorder(balloon.getNormalInset(), balloon.getNormalInset(), balloon.getNormalInset(), balloon.getPointerLength(this) + balloon.getNormalInset());
841 void setRecToRelativePosition(Rectangle rec, Point targetPoint) {
842 rec.setLocation(targetPoint.x - rec.width, targetPoint.y - rec.height / 2);
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);
850 protected void convertBoundsToContent(final Rectangle bounds, final BalloonImpl balloon) {
851 bounds.width -= balloon.getPointerLength(this);
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();
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());
869 private class CloseButton extends NonOpaquePanel {
871 private BaseButtonBehavior myButton;
873 private CloseButton() {
874 myButton = new BaseButtonBehavior(this, TimedDeadzone.NULL) {
875 protected void execute(MouseEvent e) {
876 //noinspection SSBasedInspection
877 SwingUtilities.invokeLater(new Runnable() {
879 BalloonImpl.this.hide();
888 protected void paintComponent(Graphics g) {
889 super.paintComponent(g);
891 if (!myEnableCloseButton) return;
893 if (getWidth() > 0 && myLastMoveWasInsideBalloon) {
894 final boolean pressed = myButton.isPressedByMouse();
895 getCloseButton().paintIcon(this, g, (pressed ? 1 : 0), (pressed ? 1 : 0));
900 private class MyComponent extends JPanel {
902 private BufferedImage myImage;
903 private float myAlpha;
904 private final BalloonImpl myBalloon;
906 private final Wrapper myContent;
908 private MyComponent(JComponent content, BalloonImpl balloon, EmptyBorder shapeBorder) {
913 myContent = new Wrapper(content);
914 myContent.setBorder(shapeBorder);
915 myContent.setOpaque(false);
919 myCloseRec = new CloseButton();
922 public void clear() {
928 public void doLayout() {
929 Insets insets = getInsets();
930 if (insets == null) {
931 insets = new Insets(0, 0, 0, 0);
934 myContent.setBounds(insets.left, insets.top, getWidth() - insets.left - insets.right, getHeight() - insets.top - insets.bottom);
938 public Dimension getPreferredSize() {
939 return addInsets(myContent.getPreferredSize());
943 public Dimension getMinimumSize() {
944 return addInsets(myContent.getMinimumSize());
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);
958 protected void paintComponent(final Graphics g) {
959 super.paintComponent(g);
961 final Graphics2D g2d = (Graphics2D)g;
963 final Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this);
965 Rectangle shapeBounds = myContent.getBounds();
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);
972 if (myImage != null && myAlpha != -1) {
973 g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, myAlpha));
975 g2d.drawImage(myImage, 0, 0, null);
978 myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)g, pointTarget);
984 public void removeNotify() {
985 super.removeNotify();
987 if (myLayeredPane != null) {
988 final JLayeredPane pane = myLayeredPane;
989 SwingUtilities.invokeLater(new Runnable() {
992 pane.remove(myCloseRec);
998 public void setAlpha(float alpha) {
1000 paintImmediately(0, 0, getWidth(), getHeight());
1003 public void _setBounds(Rectangle bounds) {
1004 super.setBounds(bounds);
1005 if (myCloseRec.getParent() == null && getParent() != null) {
1006 myLayeredPane.add(myCloseRec, JLayeredPane.DRAG_LAYER);
1009 if (isVisible() && myCloseRec.isVisible()) {
1010 Rectangle lpBounds = SwingUtilities.convertRectangle(getParent(), bounds, myLayeredPane);
1011 lpBounds = myPosition.getPointlessContentRec(lpBounds, myBalloon.getPointerLength(myPosition));
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);
1018 myCloseRec.setBounds(r);
1023 public void repaintButton() {
1024 myCloseRec.repaint();
1028 private static class Shaper {
1029 private final GeneralPath myPath = new GeneralPath();
1032 private final int myTargetSide;
1033 private final BalloonImpl myBalloon;
1035 public Shaper(BalloonImpl balloon, Rectangle bounds, Point targetPoint, int targetSide) {
1036 myBalloon = balloon;
1038 myTargetSide = targetSide;
1042 private void start(Point start) {
1043 myPath.moveTo(start.x, start.y);
1046 public Shaper roundUpRight() {
1047 myPath.quadTo(getCurrent().x, getCurrent().y - myBalloon.getArc(), getCurrent().x + myBalloon.getArc(),
1048 getCurrent().y - myBalloon.getArc());
1052 public Shaper roundRightDown() {
1053 myPath.quadTo(getCurrent().x + myBalloon.getArc(), getCurrent().y, getCurrent().x + myBalloon.getArc(),
1054 getCurrent().y + myBalloon.getArc());
1058 public Shaper roundLeftUp() {
1059 myPath.quadTo(getCurrent().x - myBalloon.getArc(), getCurrent().y, getCurrent().x - myBalloon.getArc(),
1060 getCurrent().y - myBalloon.getArc());
1064 public Shaper roundLeftDown() {
1065 myPath.quadTo(getCurrent().x, getCurrent().y + myBalloon.getArc(), getCurrent().x - myBalloon.getArc(),
1066 getCurrent().y + myBalloon.getArc());
1070 public Point getCurrent() {
1071 return new Point((int)myPath.getCurrentPoint().getX(), (int)myPath.getCurrentPoint().getY());
1074 public Shaper line(final int deltaX, final int deltaY) {
1075 myPath.lineTo(getCurrent().x + deltaX, getCurrent().y + deltaY);
1079 public Shaper lineTo(final int x, final int y) {
1080 myPath.lineTo(x, y);
1085 private int getTargetDelta(int effectiveSide) {
1086 return effectiveSide == myTargetSide ? myBalloon.getPointerLength(myBalloon.myPosition) : 0;
1089 public Shaper toRightCurve() {
1090 myPath.lineTo((int)myBounds.getMaxX() - myBalloon.getArc() - getTargetDelta(SwingUtilities.RIGHT) - 1, getCurrent().y);
1094 public Shaper toBottomCurve() {
1095 myPath.lineTo(getCurrent().x, (int)myBounds.getMaxY() - myBalloon.getArc() - getTargetDelta(SwingUtilities.BOTTOM) - 1);
1099 public Shaper toLeftCurve() {
1100 myPath.lineTo((int)myBounds.getX() + myBalloon.getArc() + getTargetDelta(SwingUtilities.LEFT), getCurrent().y);
1104 public Shaper toTopCurve() {
1105 myPath.lineTo(getCurrent().x, (int)myBounds.getY() + myBalloon.getArc() + getTargetDelta(SwingUtilities.TOP));
1109 public void close() {
1113 public Shape getShape() {
1118 public static void main(String[] args) {
1119 IconLoader.activate();
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);
1127 final JTree tree = new Tree();
1131 final Ref<BalloonImpl> balloon = new Ref<BalloonImpl>();
1133 tree.addMouseListener(new MouseAdapter() {
1135 public void mousePressed(final MouseEvent e) {
1136 if (balloon.get() != null && balloon.get().isVisible()) {
1137 balloon.get().dispose();
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);
1146 //pane.setBorder(new LineBorder(Color.blue));
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);
1151 if (e.isShiftDown()) {
1152 balloon.get().show(new RelativePoint(e), BalloonImpl.ABOVE);
1154 else if (e.isAltDown()) {
1155 balloon.get().show(new RelativePoint(e), BalloonImpl.BELOW);
1157 else if (e.isMetaDown()) {
1158 balloon.get().show(new RelativePoint(e), BalloonImpl.AT_LEFT);
1161 balloon.get().show(new RelativePoint(e), BalloonImpl.AT_RIGHT);
1167 tree.addMouseMotionListener(new MouseMotionAdapter() {
1169 public void mouseMoved(MouseEvent e) {
1170 System.out.println(e.getPoint());
1174 frame.setBounds(300, 300, 300, 300);
1179 public boolean wasFadedIn() {
1184 public boolean wasFadedOut() {