| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ui; |
| |
| import com.intellij.icons.AllIcons; |
| import com.intellij.ide.IdeEventQueue; |
| import com.intellij.ide.IdeTooltip; |
| import com.intellij.openapi.actionSystem.ActionManager; |
| import com.intellij.openapi.actionSystem.AnAction; |
| import com.intellij.openapi.actionSystem.AnActionEvent; |
| import com.intellij.openapi.actionSystem.DataContext; |
| import com.intellij.openapi.actionSystem.ex.AnActionListener; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.ui.DialogWrapper; |
| import com.intellij.openapi.ui.GraphicsConfig; |
| import com.intellij.openapi.ui.impl.ShadowBorderPainter; |
| import com.intellij.openapi.ui.popup.Balloon; |
| import com.intellij.openapi.ui.popup.JBPopupListener; |
| import com.intellij.openapi.ui.popup.LightweightWindowEvent; |
| import com.intellij.openapi.util.*; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.openapi.wm.FocusRequestor; |
| import com.intellij.openapi.wm.IdeFocusManager; |
| import com.intellij.openapi.wm.IdeGlassPaneUtil; |
| import com.intellij.openapi.wm.impl.IdeGlassPaneEx; |
| import com.intellij.ui.awt.RelativePoint; |
| import com.intellij.ui.components.panels.NonOpaquePanel; |
| import com.intellij.ui.components.panels.Wrapper; |
| import com.intellij.util.Alarm; |
| import com.intellij.util.IJSwingUtilities; |
| import com.intellij.util.containers.HashSet; |
| import com.intellij.util.ui.*; |
| import org.intellij.lang.annotations.JdkConstants; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.border.EmptyBorder; |
| import javax.swing.event.HyperlinkEvent; |
| import java.awt.*; |
| import java.awt.event.*; |
| import java.awt.geom.Area; |
| import java.awt.geom.GeneralPath; |
| import java.awt.geom.Rectangle2D; |
| import java.awt.geom.RoundRectangle2D; |
| import java.awt.image.BufferedImage; |
| import java.util.Set; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| |
| public class BalloonImpl implements Balloon, IdeTooltip.Ui { |
| public static final int DIALOG_ARC = 6; |
| public static final int ARC = 3; |
| public static final int DIALOG_TOPBOTTOM_POINTER_WIDTH = 24; |
| public static final int DIALOG_POINTER_WIDTH = 17; |
| public static final int TOPBOTTOM_POINTER_WIDTH = 14; |
| public static final int POINTER_WIDTH = 11; |
| public static final int DIALOG_TOPBOTTOM_POINTER_LENGTH = 16; |
| public static final int DIALOG_POINTER_LENGTH = 14; |
| public static final int TOPBOTTOM_POINTER_LENGTH = 10; |
| public static final int POINTER_LENGTH = 8; |
| |
| private final Alarm myFadeoutAlarm = new Alarm(this); |
| private long myFadeoutRequestMillis = 0; |
| private int myFadeoutRequestDelay = 0; |
| |
| private MyComponent myComp; |
| private JLayeredPane myLayeredPane; |
| private AbstractPosition myPosition; |
| private Point myTargetPoint; |
| private final boolean myHideOnFrameResize; |
| private final boolean myHideOnLinkClick; |
| |
| private final Color myBorderColor; |
| private final Insets myBorderInsets; |
| private final Color myFillColor; |
| |
| private final Insets myContainerInsets; |
| |
| private boolean myLastMoveWasInsideBalloon; |
| |
| private Rectangle myForcedBounds; |
| |
| private CloseButton myCloseRec; |
| |
| private final AWTEventListener myAwtActivityListener = new AWTEventListener() { |
| @Override |
| public void eventDispatched(final AWTEvent e) { |
| final int id = e.getID(); |
| if (e instanceof MouseEvent) { |
| final MouseEvent me = (MouseEvent)e; |
| final boolean insideBalloon = isInsideBalloon(me); |
| |
| if (myHideOnMouse && id == MouseEvent.MOUSE_PRESSED) { |
| if (!insideBalloon && !hasModalDialog(me)) { |
| hide(); |
| } |
| return; |
| } |
| |
| if (myClickHandler != null && id == MouseEvent.MOUSE_CLICKED) { |
| if (!(me.getComponent() instanceof CloseButton) && insideBalloon) { |
| myClickHandler.actionPerformed(new ActionEvent(BalloonImpl.this, ActionEvent.ACTION_PERFORMED, "click", me.getModifiersEx())); |
| if (myCloseOnClick) { |
| hide(); |
| return; |
| } |
| } |
| } |
| |
| if (myEnableCloseButton && id == MouseEvent.MOUSE_MOVED) { |
| final boolean moveChanged = insideBalloon != myLastMoveWasInsideBalloon; |
| myLastMoveWasInsideBalloon = insideBalloon; |
| if (moveChanged) { |
| if (insideBalloon && myFadeoutAlarm.getActiveRequestCount() > 0) { //Pause hiding timer when mouse is hover |
| myFadeoutAlarm.cancelAllRequests(); |
| myFadeoutRequestDelay -= System.currentTimeMillis() - myFadeoutRequestMillis; |
| } |
| if (!insideBalloon && myFadeoutRequestDelay > 0) { |
| startFadeoutTimer(myFadeoutRequestDelay); |
| } |
| myComp.repaintButton(); |
| } |
| } |
| |
| if (UIUtil.isCloseClick((MouseEvent)e)) { |
| hide(); |
| return; |
| } |
| } |
| |
| if (myHideOnKey && e instanceof KeyEvent && id == KeyEvent.KEY_PRESSED) { |
| final KeyEvent ke = (KeyEvent)e; |
| if (ke.getKeyCode() != KeyEvent.VK_SHIFT && |
| ke.getKeyCode() != KeyEvent.VK_CONTROL && |
| ke.getKeyCode() != KeyEvent.VK_ALT && |
| ke.getKeyCode() != KeyEvent.VK_META) { |
| if (SwingUtilities.isDescendingFrom(ke.getComponent(), myComp) || ke.getComponent() == myComp) return; |
| hide(); |
| } |
| } |
| } |
| }; |
| |
| private static boolean hasModalDialog(MouseEvent e) { |
| final Component c = e.getComponent(); |
| final DialogWrapper dialog = DialogWrapper.findInstance(c); |
| return dialog != null && dialog.isModal(); |
| } |
| |
| private final long myFadeoutTime; |
| private Dimension myDefaultPrefSize; |
| private final ActionListener myClickHandler; |
| private final boolean myCloseOnClick; |
| private int myShadowSize = Registry.intValue("ide.balloon.shadow.size"); |
| |
| private final CopyOnWriteArraySet<JBPopupListener> myListeners = new CopyOnWriteArraySet<JBPopupListener>(); |
| private boolean myVisible; |
| private PositionTracker<Balloon> myTracker; |
| private int myAnimationCycle = 500; |
| |
| private boolean myFadedIn; |
| private boolean myFadedOut; |
| private final int myCalloutShift; |
| |
| private final int myPositionChangeXShift; |
| private final int myPositionChangeYShift; |
| private boolean myDialogMode; |
| private IdeFocusManager myFocusManager; |
| private final String myTitle; |
| private JLabel myTitleLabel; |
| |
| private boolean myAnimationEnabled = true; |
| private boolean myShadow = false; |
| private final Layer myLayer; |
| private final boolean myBlockClicks; |
| private RelativePoint myPrevMousePoint = null; |
| |
| public boolean isInsideBalloon(MouseEvent me) { |
| return isInside(new RelativePoint(me)); |
| } |
| |
| @Override |
| public boolean isInside(@NotNull RelativePoint target) { |
| Component cmp = target.getOriginalComponent(); |
| |
| if (!cmp.isShowing()) return true; |
| if (cmp == myCloseRec) return true; |
| if (UIUtil.isDescendingFrom(cmp, myComp)) return true; |
| if (myComp == null || !myComp.isShowing()) return false; |
| return myComp.contains(target.getScreenPoint().x, target.getScreenPoint().y); |
| } |
| |
| public boolean isMovingForward(RelativePoint target) { |
| try { |
| if (myComp == null || !myComp.isShowing()) return false; |
| if (myPrevMousePoint == null) return true; |
| if (myPrevMousePoint.getComponent() != target.getComponent()) return false; |
| Rectangle rectangleOnScreen = new Rectangle(myComp.getLocationOnScreen(), myComp.getSize()); |
| return ScreenUtil.isMovementTowards(myPrevMousePoint.getScreenPoint(), target.getScreenPoint(), rectangleOnScreen); |
| } |
| finally { |
| myPrevMousePoint = target; |
| } |
| } |
| |
| private final ComponentAdapter myComponentListener = new ComponentAdapter() { |
| @Override |
| public void componentResized(final ComponentEvent e) { |
| if (myHideOnFrameResize) { |
| hide(); |
| } |
| } |
| }; |
| private Animator myAnimator; |
| private boolean myShowPointer; |
| |
| private boolean myDisposed; |
| private final JComponent myContent; |
| private boolean myHideOnMouse; |
| private final boolean myHideOnKey; |
| private final boolean myHideOnAction; |
| private final boolean myEnableCloseButton; |
| |
| public BalloonImpl(@NotNull JComponent content, |
| @NotNull Color borderColor, |
| Insets borderInsets, |
| @NotNull Color fillColor, |
| boolean hideOnMouse, |
| boolean hideOnKey, |
| boolean hideOnAction, |
| boolean showPointer, |
| boolean enableCloseButton, |
| long fadeoutTime, |
| boolean hideOnFrameResize, |
| boolean hideOnLinkClick, |
| ActionListener clickHandler, |
| boolean closeOnClick, |
| int animationCycle, |
| int calloutShift, |
| int positionChangeXShift, |
| int positionChangeYShift, |
| boolean dialogMode, |
| String title, |
| Insets contentInsets, |
| boolean shadow, |
| boolean smallVariant, |
| boolean blockClicks, |
| Layer layer) { |
| myBorderColor = borderColor; |
| myBorderInsets = borderInsets != null ? borderInsets : new Insets(3, 3, 3, 3); |
| myFillColor = fillColor; |
| myContent = content; |
| myHideOnMouse = hideOnMouse; |
| myHideOnKey = hideOnKey; |
| myHideOnAction = hideOnAction; |
| myShowPointer = showPointer; |
| myEnableCloseButton = enableCloseButton; |
| myHideOnFrameResize = hideOnFrameResize; |
| myHideOnLinkClick = hideOnLinkClick; |
| myClickHandler = clickHandler; |
| myCloseOnClick = closeOnClick; |
| myCalloutShift = calloutShift; |
| myPositionChangeXShift = positionChangeXShift; |
| myPositionChangeYShift = positionChangeYShift; |
| myDialogMode = dialogMode; |
| myTitle = title; |
| myLayer = layer != null ? layer : Layer.normal; |
| myBlockClicks = blockClicks; |
| |
| if (!myDialogMode) { |
| new AwtVisitor(content) { |
| @Override |
| public boolean visit(Component component) { |
| if (component instanceof JLabel) { |
| JLabel label = (JLabel)component; |
| if (label.getDisplayedMnemonic() != '\0' || label.getDisplayedMnemonicIndex() >= 0) { |
| myDialogMode = true; |
| return true; |
| } |
| } else if (component instanceof JCheckBox) { |
| JCheckBox checkBox = (JCheckBox)component; |
| if (checkBox.getMnemonic() >= 0 || checkBox.getDisplayedMnemonicIndex() >= 0) { |
| myDialogMode = true; |
| return true; |
| } |
| } |
| return false; |
| } |
| }; |
| } |
| |
| myShadow = shadow; |
| myShadowSize = Registry.intValue("ide.balloon.shadow.size"); |
| myContainerInsets = contentInsets; |
| |
| myFadeoutTime = fadeoutTime; |
| myAnimationCycle = animationCycle; |
| |
| if (smallVariant) { |
| new AwtVisitor(myContent) { |
| @Override |
| public boolean visit(Component component) { |
| UIUtil.applyStyle(UIUtil.ComponentStyle.SMALL, component); |
| return false; |
| } |
| }; |
| } |
| } |
| |
| @Override |
| public void show(final RelativePoint target, final Balloon.Position position) { |
| AbstractPosition pos = getAbstractPositionFor(position); |
| |
| show(target, pos); |
| } |
| |
| public int getLayer() { |
| Integer result = JLayeredPane.DEFAULT_LAYER; |
| switch (myLayer) { |
| case normal: |
| result = JLayeredPane.POPUP_LAYER; |
| break; |
| case top: |
| result = JLayeredPane.DRAG_LAYER; |
| break; |
| } |
| |
| return result; |
| } |
| |
| private static AbstractPosition getAbstractPositionFor(Position position) { |
| AbstractPosition pos = BELOW; |
| switch (position) { |
| case atLeft: |
| pos = AT_LEFT; |
| break; |
| case atRight: |
| pos = AT_RIGHT; |
| break; |
| case below: |
| pos = BELOW; |
| break; |
| case above: |
| pos = ABOVE; |
| break; |
| } |
| return pos; |
| } |
| |
| @Override |
| public void show(PositionTracker<Balloon> tracker, Balloon.Position position) { |
| AbstractPosition pos = BELOW; |
| switch (position) { |
| case atLeft: |
| pos = AT_LEFT; |
| break; |
| case atRight: |
| pos = AT_RIGHT; |
| break; |
| case below: |
| pos = BELOW; |
| break; |
| case above: |
| pos = ABOVE; |
| break; |
| } |
| |
| show(tracker, pos); |
| } |
| |
| private Insets getInsetsCopy() { |
| return new Insets(myBorderInsets.top, myBorderInsets.left, myBorderInsets.bottom, myBorderInsets.right); |
| } |
| |
| private void show(RelativePoint target, AbstractPosition position) { |
| show(new PositionTracker.Static<Balloon>(target), position); |
| } |
| |
| private void show(PositionTracker<Balloon> tracker, AbstractPosition position) { |
| assert !myDisposed : "Balloon is already disposed"; |
| |
| if (isVisible()) return; |
| final Component comp = tracker.getComponent(); |
| if (!comp.isShowing()) return; |
| |
| myTracker = tracker; |
| myTracker.init(this); |
| |
| JRootPane root = null; |
| JDialog dialog = IJSwingUtilities.findParentOfType(comp, JDialog.class); |
| if (dialog != null) { |
| root = dialog.getRootPane(); |
| } else { |
| JWindow jwindow = IJSwingUtilities.findParentOfType(comp, JWindow.class); |
| if (jwindow != null) { |
| root = jwindow.getRootPane(); |
| } else { |
| JFrame frame = IJSwingUtilities.findParentOfType(comp, JFrame.class); |
| if (frame != null) { |
| root = frame.getRootPane(); |
| } else { |
| assert false; |
| } |
| } |
| } |
| |
| myVisible = true; |
| |
| myLayeredPane = root.getLayeredPane(); |
| myPosition = position; |
| UIUtil.setFutureRootPane(myContent, root); |
| |
| myFocusManager = IdeFocusManager.findInstanceByComponent(myLayeredPane); |
| final Ref<Component> originalFocusOwner = new Ref<Component>(); |
| final Ref<FocusRequestor> focusRequestor = new Ref<FocusRequestor>(); |
| final Ref<ActionCallback> proxyFocusRequest = new Ref<ActionCallback>(new ActionCallback.Done()); |
| |
| boolean mnemonicsFix = myDialogMode && SystemInfo.isMac && Registry.is("ide.mac.inplaceDialogMnemonicsFix"); |
| if (mnemonicsFix) { |
| final IdeGlassPaneEx glassPane = (IdeGlassPaneEx)IdeGlassPaneUtil.find(myLayeredPane); |
| assert glassPane != null; |
| |
| proxyFocusRequest.set(new ActionCallback()); |
| |
| myFocusManager.doWhenFocusSettlesDown(new ExpirableRunnable() { |
| @Override |
| public boolean isExpired() { |
| return isDisposed(); |
| } |
| |
| @Override |
| public void run() { |
| IdeEventQueue.getInstance().disableInputMethods(BalloonImpl.this); |
| originalFocusOwner.set(myFocusManager.getFocusOwner()); |
| myFocusManager.requestFocus(glassPane.getProxyComponent(), true).notify(proxyFocusRequest.get()); |
| focusRequestor.set(myFocusManager.getFurtherRequestor()); |
| } |
| }); |
| } |
| |
| myLayeredPane.addComponentListener(myComponentListener); |
| |
| myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutShift); |
| |
| int positionChangeFix = 0; |
| if (myShowPointer) { |
| Rectangle rec = getRecForPosition(myPosition, true); |
| |
| if (!myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc())) { |
| rec = getRecForPosition(myPosition, false); |
| |
| Rectangle lp = new Rectangle(new Point(myContainerInsets.left, myContainerInsets.top), myLayeredPane.getSize()); |
| lp.width -= myContainerInsets.right; |
| lp.height -= myContainerInsets.bottom; |
| |
| if (!lp.contains(rec)) { |
| Rectangle2D currentSquare = lp.createIntersection(rec); |
| |
| double maxSquare = currentSquare.getWidth() * currentSquare.getHeight(); |
| AbstractPosition targetPosition = myPosition; |
| |
| for (AbstractPosition eachPosition : myPosition.getOtherPositions()) { |
| Rectangle2D eachIntersection = lp.createIntersection(getRecForPosition(eachPosition, false)); |
| double eachSquare = eachIntersection.getWidth() * eachIntersection.getHeight(); |
| if (maxSquare < eachSquare) { |
| maxSquare = eachSquare; |
| targetPosition = eachPosition; |
| } |
| } |
| |
| myPosition = targetPosition; |
| positionChangeFix = myPosition.getChangeShift(position, myPositionChangeXShift, myPositionChangeYShift); |
| } |
| } |
| } |
| |
| if (myPosition != position) { |
| myTargetPoint = myPosition.getShiftedPoint(myTracker.recalculateLocation(this).getPoint(myLayeredPane), myCalloutShift > 0 ? myCalloutShift + positionChangeFix : positionChangeFix); |
| } |
| |
| createComponent(); |
| |
| myComp.validate(); |
| |
| Rectangle rec = myComp.getContentBounds(); |
| |
| if (myShowPointer && !myPosition.isOkToHavePointer(myTargetPoint, rec, getPointerLength(myPosition), getPointerWidth(myPosition), getArc())) { |
| myShowPointer = false; |
| myComp.removeAll(); |
| myLayeredPane.remove(myComp); |
| |
| createComponent(); |
| if (!new Rectangle(myLayeredPane.getSize()).contains(new Rectangle(myComp.getSize()))) { // Balloon is bigger than window, don't show it at all. |
| myLayeredPane = null; |
| hide(); |
| return; |
| } |
| } |
| |
| for (JBPopupListener each : myListeners) { |
| each.beforeShown(new LightweightWindowEvent(this)); |
| } |
| |
| runAnimation(true, myLayeredPane, null); |
| |
| myLayeredPane.revalidate(); |
| myLayeredPane.repaint(); |
| |
| if (mnemonicsFix) { |
| proxyFocusRequest.get().doWhenDone(new Runnable() { |
| @Override |
| public void run() { |
| myFocusManager.requestFocus(originalFocusOwner.get(), true); |
| } |
| }); |
| } |
| |
| Toolkit.getDefaultToolkit().addAWTEventListener( |
| myAwtActivityListener, AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.KEY_EVENT_MASK); |
| |
| if (ApplicationManager.getApplication() != null) { |
| ActionManager.getInstance().addAnActionListener(new AnActionListener.Adapter() { |
| @Override |
| public void beforeActionPerformed(AnAction action, DataContext dataContext, AnActionEvent event) { |
| if (myHideOnAction) { |
| hide(); |
| } |
| } |
| }, this); |
| } |
| |
| if (myHideOnLinkClick) { |
| final Ref<JEditorPane> ref = Ref.create(null); |
| new AwtVisitor(myContent) { |
| @Override |
| public boolean visit(Component component) { |
| if (component instanceof JEditorPane) { |
| ref.set((JEditorPane)component); |
| return true; |
| } |
| return false; |
| } |
| }; |
| if (!ref.isNull()) { |
| ref.get().addHyperlinkListener(new HyperlinkAdapter() { |
| @Override |
| protected void hyperlinkActivated(HyperlinkEvent e) { |
| hide(); |
| } |
| }); |
| } |
| } |
| } |
| |
| private Rectangle getRecForPosition(AbstractPosition position, boolean adjust) { |
| Dimension size = getContentSizeFor(position); |
| Rectangle rec = new Rectangle(new Point(0, 0), size); |
| |
| position.setRecToRelativePosition(rec, myTargetPoint); |
| |
| if (adjust) { |
| rec = myPosition.getUpdatedBounds(myLayeredPane.getSize(), myForcedBounds, rec.getSize(), myShowPointer, myTargetPoint, |
| myContainerInsets); |
| } |
| |
| return rec; |
| } |
| |
| private Dimension getContentSizeFor(AbstractPosition position) { |
| Insets insets = position.createBorder(this).getBorderInsets(); |
| if (insets == null) { |
| insets = new Insets(0, 0, 0, 0); |
| } |
| |
| Dimension size = myContent.getPreferredSize(); |
| size.width += insets.left + insets.right; |
| size.height += insets.top + insets.bottom; |
| |
| return size; |
| } |
| |
| private void disposeCloseButton(CloseButton closeButton) { |
| if (closeButton != null && closeButton.getParent() != null) { |
| Container parent = closeButton.getParent(); |
| parent.remove(closeButton); |
| //noinspection RedundantCast |
| ((JComponent)parent).revalidate(); |
| parent.repaint(); |
| } |
| } |
| |
| private void createComponent() { |
| myComp = new MyComponent(myContent, this, myShowPointer ? myPosition.createBorder(this) : getPointlessBorder()); |
| myCloseRec = new CloseButton(); |
| |
| myComp.clear(); |
| myComp.myAlpha = myAnimationEnabled ? 0f : -1; |
| |
| final int borderSize = getShadowBorderSize(); |
| myComp.setBorder(new EmptyBorder(borderSize, borderSize, borderSize, borderSize)); |
| |
| myLayeredPane.add(myComp); |
| myLayeredPane.setLayer(myComp, getLayer(), 0); // the second balloon must be over the first one |
| myPosition.updateBounds(this); |
| if (myBlockClicks) { |
| myComp.addMouseListener(new MouseAdapter() { |
| @Override |
| public void mouseClicked(MouseEvent e) { |
| e.consume(); |
| } |
| |
| @Override |
| public void mousePressed(MouseEvent e) { |
| e.consume(); |
| } |
| |
| @Override |
| public void mouseReleased(MouseEvent e) { |
| e.consume(); |
| } |
| }); |
| } |
| } |
| |
| |
| @NotNull |
| private EmptyBorder getPointlessBorder() { |
| return new EmptyBorder(myBorderInsets); |
| } |
| |
| @Override |
| public void revalidate() { |
| revalidate(myTracker); |
| } |
| |
| @Override |
| public void revalidate(@NotNull PositionTracker<Balloon> tracker) { |
| RelativePoint newPosition = tracker.recalculateLocation(this); |
| |
| if (newPosition != null) { |
| myTargetPoint = myPosition.getShiftedPoint(newPosition.getPoint(myLayeredPane), myCalloutShift); |
| myPosition.updateBounds(this); |
| } |
| } |
| |
| public int getShadowBorderSize() { |
| return hasShadow() ? myShadowSize : 0; |
| } |
| |
| public boolean hasShadow() { |
| return myShadow && Registry.is("ide.balloon.shadowEnabled"); |
| } |
| |
| @Override |
| public void show(JLayeredPane pane) { |
| show(pane, null); |
| } |
| |
| @Override |
| public void showInCenterOf(JComponent component) { |
| final Dimension size = component.getSize(); |
| show(new RelativePoint(component, new Point(size.width/2, size.height/2)), Balloon.Position.above); |
| } |
| |
| public void show(JLayeredPane pane, @Nullable Rectangle bounds) { |
| if (bounds != null) { |
| myForcedBounds = bounds; |
| } |
| show(new RelativePoint(pane, new Point(0, 0)), Balloon.Position.above); |
| } |
| |
| |
| private void runAnimation(boolean forward, final JLayeredPane layeredPane, @Nullable final Runnable onDone) { |
| if (myAnimator != null) { |
| Disposer.dispose(myAnimator); |
| } |
| myAnimator = new Animator("Balloon", 8, myAnimationEnabled ? myAnimationCycle : 0, false, forward) { |
| @Override |
| public void paintNow(final int frame, final int totalFrames, final int cycle) { |
| if (myComp == null || myComp.getParent() == null || !myAnimationEnabled) return; |
| myComp.setAlpha((float)frame / totalFrames); |
| } |
| |
| @Override |
| protected void paintCycleEnd() { |
| if (myComp == null || myComp.getParent() == null) return; |
| |
| if (isForward()) { |
| myComp.clear(); |
| myComp.repaint(); |
| |
| myFadedIn = true; |
| |
| startFadeoutTimer((int)myFadeoutTime); |
| } |
| else { |
| layeredPane.remove(myComp); |
| layeredPane.revalidate(); |
| layeredPane.repaint(); |
| } |
| Disposer.dispose(this); |
| } |
| |
| @Override |
| public void dispose() { |
| super.dispose(); |
| myAnimator = null; |
| if (onDone != null) { |
| onDone.run(); |
| } |
| } |
| }; |
| |
| myAnimator.resume(); |
| } |
| |
| public void startFadeoutTimer(final int fadeoutDelay) { |
| if (fadeoutDelay > 0) { |
| myFadeoutAlarm.cancelAllRequests(); |
| myFadeoutRequestMillis = System.currentTimeMillis(); |
| myFadeoutRequestDelay = fadeoutDelay; |
| myFadeoutAlarm.addRequest(new Runnable() { |
| @Override |
| public void run() { |
| hide(); |
| } |
| }, fadeoutDelay, null); |
| } |
| } |
| |
| |
| int getArc() { |
| return myDialogMode ? DIALOG_ARC : ARC; |
| } |
| |
| int getPointerWidth(AbstractPosition position) { |
| if (myDialogMode) { |
| return position.isTopBottomPointer() ? DIALOG_TOPBOTTOM_POINTER_WIDTH : DIALOG_POINTER_WIDTH; |
| } else { |
| return position.isTopBottomPointer() ? TOPBOTTOM_POINTER_WIDTH : POINTER_WIDTH; |
| } |
| } |
| |
| public static int getNormalInset() { |
| return 3; |
| } |
| |
| int getPointerLength(AbstractPosition position) { |
| return getPointerLength(position, myDialogMode); |
| } |
| |
| static int getPointerLength(AbstractPosition position, boolean dialogMode) { |
| if (dialogMode) { |
| return position.isTopBottomPointer() ? DIALOG_TOPBOTTOM_POINTER_LENGTH : DIALOG_POINTER_LENGTH; |
| } else { |
| return position.isTopBottomPointer() ? TOPBOTTOM_POINTER_LENGTH : POINTER_LENGTH; |
| } |
| } |
| |
| public static int getPointerLength(Position position, boolean dialogMode) { |
| return getPointerLength(getAbstractPositionFor(position), dialogMode); |
| } |
| |
| @Override |
| public void hide() { |
| hide(false); |
| } |
| |
| @Override |
| public void hide(boolean ok) { |
| hideAndDispose(ok); |
| } |
| |
| @Override |
| public void dispose() { |
| hideAndDispose(false); |
| } |
| |
| private void hideAndDispose(final boolean ok) { |
| if (myDisposed) return; |
| myDisposed = true; |
| |
| final Runnable disposeRunnable = new Runnable() { |
| @Override |
| public void run() { |
| myFadedOut = true; |
| |
| for (JBPopupListener each : myListeners) { |
| each.onClosed(new LightweightWindowEvent(BalloonImpl.this, ok)); |
| } |
| |
| Disposer.dispose(BalloonImpl.this); |
| onDisposed(); |
| } |
| }; |
| |
| Toolkit.getDefaultToolkit().removeAWTEventListener(myAwtActivityListener); |
| if (myLayeredPane != null) { |
| myLayeredPane.removeComponentListener(myComponentListener); |
| Disposer.register(ApplicationManager.getApplication(), this); // to be safe if Application suddenly exits and animation wouldn't have a chance to complete |
| |
| runAnimation(false, myLayeredPane, new Runnable() { |
| @Override |
| public void run() { |
| disposeRunnable.run(); |
| } |
| }); |
| } |
| else { |
| disposeRunnable.run(); |
| } |
| |
| myVisible = false; |
| myTracker = null; |
| } |
| |
| protected void onDisposed() { } |
| |
| @Override |
| public void addListener(@NotNull JBPopupListener listener) { |
| myListeners.add(listener); |
| } |
| |
| public boolean isVisible() { |
| return myVisible; |
| } |
| |
| public void setHideOnClickOutside(boolean hideOnMouse) { |
| myHideOnMouse = hideOnMouse; |
| } |
| |
| public void setShowPointer(final boolean show) { |
| myShowPointer = show; |
| } |
| |
| @SuppressWarnings("MethodMayBeStatic") |
| public Icon getCloseButton() { |
| return AllIcons.General.BalloonClose; |
| } |
| |
| @Override |
| public void setBounds(Rectangle bounds) { |
| myForcedBounds = bounds; |
| if (myPosition != null) { |
| myPosition.updateBounds(this); |
| } |
| } |
| |
| public void setShadowSize(int shadowSize) { |
| myShadowSize = shadowSize; |
| } |
| |
| @Override |
| public Dimension getPreferredSize() { |
| if (myComp != null) { |
| return myComp.getPreferredSize(); |
| } |
| if (myDefaultPrefSize == null) { |
| final EmptyBorder border = getPointlessBorder(); |
| final MyComponent c = new MyComponent(myContent, this, border); |
| myDefaultPrefSize = c.getPreferredSize(); |
| } |
| return myDefaultPrefSize; |
| } |
| |
| private abstract static class AbstractPosition { |
| abstract EmptyBorder createBorder(final BalloonImpl balloon); |
| |
| |
| abstract void setRecToRelativePosition(Rectangle rec, Point targetPoint); |
| |
| abstract int getChangeShift(AbstractPosition original, int xShift, int yShift); |
| |
| public void updateBounds(final BalloonImpl balloon) { |
| final Rectangle bounds = |
| getUpdatedBounds(balloon.myLayeredPane.getSize(), balloon.myForcedBounds, balloon.myComp.getPreferredSize(), balloon.myShowPointer, |
| balloon.myTargetPoint, balloon.myContainerInsets); |
| |
| final Point point = getShiftedPoint(bounds.getLocation(), -balloon.getShadowBorderSize()); |
| bounds.setLocation(point); |
| balloon.myComp._setBounds(bounds); |
| } |
| |
| public Rectangle getUpdatedBounds(Dimension layeredPaneSize, |
| Rectangle forcedBounds, |
| Dimension preferredSize, |
| boolean showPointer, |
| Point point, Insets containerInsets) { |
| |
| Rectangle bounds = forcedBounds; |
| |
| if (bounds == null) { |
| Point location = showPointer |
| ? getLocation(layeredPaneSize, point, preferredSize) |
| : new Point(point.x - preferredSize.width / 2, point.y - preferredSize.height / 2); |
| bounds = new Rectangle(location.x, location.y, preferredSize.width, preferredSize.height); |
| |
| ScreenUtil.moveToFit(bounds, new Rectangle(0, 0, layeredPaneSize.width, layeredPaneSize.height), containerInsets); |
| } |
| |
| return bounds; |
| } |
| |
| abstract Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize); |
| |
| void paintComponent(BalloonImpl balloon, final Rectangle bounds, final Graphics2D g, Point pointTarget) { |
| final GraphicsConfig cfg = new GraphicsConfig(g); |
| cfg.setAntialiasing(true); |
| |
| Shape shape; |
| if (balloon.myShowPointer) { |
| shape = getPointingShape(bounds, pointTarget, balloon); |
| } |
| else { |
| shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, balloon.getArc(), balloon.getArc()); |
| } |
| |
| g.setColor(balloon.myFillColor); |
| g.fill(shape); |
| g.setColor(balloon.myBorderColor); |
| |
| if (balloon.myTitleLabel != null) { |
| Rectangle titleBounds = balloon.myTitleLabel.getBounds(); |
| |
| final int shadow = balloon.getShadowBorderSize(); |
| Insets inset = getTitleInsets(getNormalInset() - 1 + shadow, balloon.getPointerLength(this) + 50 + shadow); |
| |
| titleBounds.x -= inset.left + 1; |
| titleBounds.width += inset.left + inset.right + 50; |
| titleBounds.y -= inset.top + 1; |
| titleBounds.height += inset.top + inset.bottom + 1; |
| |
| Area area = new Area(shape); |
| area.intersect(new Area(titleBounds)); |
| |
| |
| Color fgColor = UIManager.getColor("Label.foreground"); |
| fgColor = ColorUtil.toAlpha(fgColor, 140); |
| g.setColor(fgColor); |
| g.fill(area); |
| |
| g.setColor(balloon.myBorderColor); |
| g.draw(area); |
| |
| //Rectangle titleBounds = balloon.myTitleLabel.getBounds(); |
| //titleBounds = SwingUtilities.convertRectangle(balloon.myTitleLabel.getParent(), titleBounds, component); |
| // |
| //g.setColor(balloon.myBorderColor); |
| //int inset = balloon.getNormalInset(); |
| //g.drawLine(titleBounds.x - inset, (int)titleBounds.getMaxY(), (int)titleBounds.getMaxX() + inset, (int)titleBounds.getMaxY()); |
| } |
| |
| g.draw(shape); |
| cfg.restore(); |
| } |
| |
| protected abstract Insets getTitleInsets(int normalInset, int pointerLength); |
| |
| protected abstract Shape getPointingShape(final Rectangle bounds, |
| final Point pointTarget, |
| final BalloonImpl balloon); |
| |
| public boolean isOkToHavePointer(Point targetPoint, Rectangle bounds, int pointerLength, int pointerWidth, int arc) { |
| if (bounds.x < targetPoint.x && bounds.x + bounds.width > targetPoint.x && bounds.y < targetPoint.y && bounds.y + bounds.height < targetPoint.y) return false; |
| |
| Rectangle pointless = getPointlessContentRec(bounds, pointerLength); |
| |
| int size = getDistanceToTarget(pointless, targetPoint); |
| if (size < pointerLength - 1) return false; |
| |
| UnfairTextRange balloonRange; |
| UnfairTextRange pointerRange; |
| if (isTopBottomPointer()) { |
| balloonRange = new UnfairTextRange(bounds.x + arc, bounds.x + bounds.width - arc * 2); |
| pointerRange = new UnfairTextRange(targetPoint.x - pointerWidth / 2, targetPoint.x + pointerWidth / 2); |
| } |
| else { |
| balloonRange = new UnfairTextRange(bounds.y + arc, bounds.y + bounds.height - arc * 2); |
| pointerRange = new UnfairTextRange(targetPoint.y - pointerWidth / 2, targetPoint.y + pointerWidth / 2); |
| } |
| |
| return balloonRange.contains(pointerRange); |
| } |
| |
| protected abstract int getDistanceToTarget(Rectangle rectangle, Point targetPoint); |
| |
| protected boolean isTopBottomPointer() { |
| return this instanceof Below || this instanceof Above; |
| } |
| |
| protected abstract Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength); |
| |
| public Set<AbstractPosition> getOtherPositions() { |
| HashSet<AbstractPosition> all = new HashSet<AbstractPosition>(); |
| all.add(BELOW); |
| all.add(ABOVE); |
| all.add(AT_RIGHT); |
| all.add(AT_LEFT); |
| |
| all.remove(this); |
| |
| return all; |
| } |
| |
| public abstract Point getShiftedPoint(Point targetPoint, int shift); |
| } |
| |
| public static final AbstractPosition BELOW = new Below(); |
| public static final AbstractPosition ABOVE = new Above(); |
| public static final AbstractPosition AT_RIGHT = new AtRight(); |
| public static final AbstractPosition AT_LEFT = new AtLeft(); |
| |
| |
| private static class Below extends AbstractPosition { |
| |
| |
| @Override |
| public Point getShiftedPoint(Point targetPoint, int shift) { |
| return new Point(targetPoint.x, targetPoint.y + shift); |
| } |
| |
| @Override |
| int getChangeShift(AbstractPosition original, int xShift, int yShift) { |
| return original == ABOVE ? yShift : 0; |
| } |
| |
| |
| @Override |
| protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) { |
| return rectangle.y - targetPoint.y; |
| } |
| |
| @Override |
| protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) { |
| return new Rectangle(bounds.x, bounds.y + pointerLength, bounds.width, bounds.height - pointerLength); |
| } |
| |
| @Override |
| EmptyBorder createBorder(final BalloonImpl balloon) { |
| Insets insets = balloon.getInsetsCopy(); |
| insets.top += balloon.getPointerLength(this); |
| return new EmptyBorder(insets); |
| } |
| |
| @Override |
| void setRecToRelativePosition(Rectangle rec, Point targetPoint) { |
| rec.setLocation(new Point(targetPoint.x - rec.width / 2, targetPoint.y)); |
| } |
| |
| @Override |
| Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) { |
| final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize); |
| return new Point(center.x, targetPoint.y); |
| } |
| |
| @Override |
| protected Insets getTitleInsets(int normalInset, int pointerLength) { |
| return new Insets(pointerLength, normalInset, normalInset, normalInset); |
| } |
| |
| @Override |
| protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) { |
| final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.TOP); |
| shaper.line(balloon.getPointerWidth(this) / 2, balloon.getPointerLength(this)).toRightCurve().roundRightDown().toBottomCurve().roundLeftDown() |
| .toLeftCurve().roundLeftUp().toTopCurve().roundUpRight() |
| .lineTo(pointTarget.x - balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y); |
| shaper.close(); |
| |
| return shaper.getShape(); |
| } |
| } |
| |
| private static class Above extends AbstractPosition { |
| |
| @Override |
| public Point getShiftedPoint(Point targetPoint, int shift) { |
| return new Point(targetPoint.x, targetPoint.y - shift); |
| } |
| |
| @Override |
| int getChangeShift(AbstractPosition original, int xShift, int yShift) { |
| return original == BELOW ? -yShift : 0; |
| } |
| |
| @Override |
| protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) { |
| return targetPoint.y - (int)rectangle.getMaxY(); |
| } |
| |
| @Override |
| protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) { |
| return new Rectangle(bounds.x, bounds.y, bounds.width, bounds.height - pointerLength); |
| } |
| |
| @Override |
| EmptyBorder createBorder(final BalloonImpl balloon) { |
| Insets insets = balloon.getInsetsCopy(); |
| insets.bottom = balloon.getPointerLength(this); |
| return new EmptyBorder(insets); |
| } |
| |
| @Override |
| void setRecToRelativePosition(Rectangle rec, Point targetPoint) { |
| rec.setLocation(targetPoint.x - rec.width / 2, targetPoint.y - rec.height); |
| } |
| |
| @Override |
| Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) { |
| final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize); |
| return new Point(center.x, targetPoint.y - balloonSize.height); |
| } |
| |
| @Override |
| protected Insets getTitleInsets(int normalInset, int pointerLength) { |
| return new Insets(normalInset, normalInset, normalInset, normalInset); |
| } |
| |
| @Override |
| protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) { |
| final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.BOTTOM); |
| shaper.line(-balloon.getPointerWidth(this) / 2, -balloon.getPointerLength(this) + 1); |
| shaper.toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown().toBottomCurve().line(0, 2) |
| .roundLeftDown().lineTo(pointTarget.x + balloon.getPointerWidth(this) / 2, shaper.getCurrent().y).lineTo(pointTarget.x, pointTarget.y) |
| .close(); |
| |
| |
| return shaper.getShape(); |
| } |
| } |
| |
| private static class AtRight extends AbstractPosition { |
| |
| @Override |
| public Point getShiftedPoint(Point targetPoint, int shift) { |
| return new Point(targetPoint.x + shift, targetPoint.y); |
| } |
| |
| @Override |
| int getChangeShift(AbstractPosition original, int xShift, int yShift) { |
| return original == AT_LEFT ? xShift : 0; |
| } |
| |
| @Override |
| protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) { |
| return rectangle.x - targetPoint.x; |
| } |
| |
| @Override |
| protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) { |
| return new Rectangle(bounds.x + pointerLength, bounds.y, bounds.width - pointerLength, bounds.height); |
| } |
| |
| @Override |
| EmptyBorder createBorder(final BalloonImpl balloon) { |
| Insets insets = balloon.getInsetsCopy(); |
| insets.left += balloon.getPointerLength(this); |
| return new EmptyBorder(insets); |
| } |
| |
| @Override |
| void setRecToRelativePosition(Rectangle rec, Point targetPoint) { |
| rec.setLocation(targetPoint.x, targetPoint.y - rec.height / 2); |
| } |
| |
| @Override |
| Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) { |
| final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize); |
| return new Point(targetPoint.x, center.y); |
| } |
| |
| @Override |
| protected Insets getTitleInsets(int normalInset, int pointerLength) { |
| return new Insets(normalInset, pointerLength, normalInset, normalInset); |
| } |
| |
| @Override |
| protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) { |
| final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.LEFT); |
| shaper.line(balloon.getPointerLength(this), -balloon.getPointerWidth(this) / 2).toTopCurve().roundUpRight().toRightCurve().roundRightDown() |
| .toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp() |
| .lineTo(shaper.getCurrent().x, pointTarget.y + balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close(); |
| |
| return shaper.getShape(); |
| } |
| } |
| |
| private static class AtLeft extends AbstractPosition { |
| |
| @Override |
| public Point getShiftedPoint(Point targetPoint, int shift) { |
| return new Point(targetPoint.x - shift, targetPoint.y); |
| } |
| |
| @Override |
| int getChangeShift(AbstractPosition original, int xShift, int yShift) { |
| return original == AT_RIGHT ? -xShift : 0; |
| } |
| |
| |
| @Override |
| protected int getDistanceToTarget(Rectangle rectangle, Point targetPoint) { |
| return targetPoint.x - (int)rectangle.getMaxX(); |
| } |
| |
| @Override |
| protected Rectangle getPointlessContentRec(Rectangle bounds, int pointerLength) { |
| return new Rectangle(bounds.x, bounds.y, bounds.width - pointerLength, bounds.height); |
| } |
| |
| @Override |
| EmptyBorder createBorder(final BalloonImpl balloon) { |
| Insets insets = balloon.getInsetsCopy(); |
| insets.right += balloon.getPointerLength(this); |
| return new EmptyBorder(insets); |
| } |
| |
| @Override |
| void setRecToRelativePosition(Rectangle rec, Point targetPoint) { |
| rec.setLocation(targetPoint.x - rec.width, targetPoint.y - rec.height / 2); |
| } |
| |
| @Override |
| Point getLocation(final Dimension containerSize, final Point targetPoint, final Dimension balloonSize) { |
| final Point center = UIUtil.getCenterPoint(new Rectangle(targetPoint, new Dimension(0, 0)), balloonSize); |
| return new Point(targetPoint.x - balloonSize.width, center.y); |
| } |
| |
| @Override |
| protected Insets getTitleInsets(int normalInset, int pointerLength) { |
| return new Insets(normalInset, pointerLength, normalInset, normalInset); |
| } |
| |
| @Override |
| protected Shape getPointingShape(final Rectangle bounds, final Point pointTarget, final BalloonImpl balloon) { |
| final Shaper shaper = new Shaper(balloon, bounds, pointTarget, SwingConstants.RIGHT); |
| shaper.lineTo((int)bounds.getMaxX() - shaper.getTargetDelta(SwingConstants.RIGHT) - 1, pointTarget.y + balloon.getPointerWidth(this) / 2); |
| shaper.toBottomCurve().roundLeftDown().toLeftCurve().roundLeftUp().toTopCurve().roundUpRight().toRightCurve().roundRightDown() |
| .lineTo(shaper.getCurrent().x, pointTarget.y - balloon.getPointerWidth(this) / 2).lineTo(pointTarget.x, pointTarget.y).close(); |
| return shaper.getShape(); |
| } |
| } |
| |
| private class CloseButton extends NonOpaquePanel { |
| |
| private final BaseButtonBehavior myButton; |
| |
| private CloseButton() { |
| myButton = new BaseButtonBehavior(this, TimedDeadzone.NULL) { |
| @Override |
| protected void execute(MouseEvent e) { |
| if (!myEnableCloseButton) return; |
| //noinspection SSBasedInspection |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| BalloonImpl.this.hide(); |
| } |
| }); |
| } |
| }; |
| |
| if (!myEnableCloseButton) { |
| setVisible(false); |
| } |
| } |
| |
| @Override |
| protected void paintComponent(Graphics g) { |
| super.paintComponent(g); |
| |
| if (!myEnableCloseButton) return; |
| |
| if (getWidth() > 0 && myLastMoveWasInsideBalloon) { |
| final boolean pressed = myButton.isPressedByMouse(); |
| getCloseButton().paintIcon(this, g, pressed ? 1 : 0, pressed ? 1 : 0); |
| } |
| } |
| } |
| |
| private class MyComponent extends JPanel implements ComponentWithMnemonics { |
| |
| private BufferedImage myImage; |
| private float myAlpha; |
| private final BalloonImpl myBalloon; |
| |
| private final JComponent myContent; |
| private ShadowBorderPainter.Shadow myShadow; |
| |
| private MyComponent(JComponent content, BalloonImpl balloon, EmptyBorder shapeBorder) { |
| setOpaque(false); |
| setLayout(null); |
| myBalloon = balloon; |
| |
| setFocusCycleRoot(true); |
| putClientProperty(Balloon.KEY, BalloonImpl.this); |
| |
| myContent = new JPanel(new BorderLayout(2, 2)); |
| Wrapper contentWrapper = new Wrapper(content); |
| if (myTitle != null) { |
| myTitleLabel = new JLabel(myTitle, SwingConstants.CENTER); |
| myTitleLabel.setForeground(UIManager.getColor("List.background")); |
| myTitleLabel.setBorder(new EmptyBorder(0, 4, 0, 4)); |
| myContent.add(myTitleLabel, BorderLayout.NORTH); |
| contentWrapper.setBorder(new EmptyBorder(1, 1, 1, 1)); |
| } |
| myContent.add(contentWrapper, BorderLayout.CENTER); |
| myContent.setBorder(shapeBorder); |
| myContent.setOpaque(false); |
| |
| add(myContent); |
| |
| |
| } |
| |
| public Rectangle getContentBounds() { |
| final Rectangle bounds = super.getBounds(); |
| final int shadow = myBalloon.getShadowBorderSize(); |
| bounds.x += shadow; |
| bounds.width -= shadow * 2; |
| bounds.y += shadow; |
| bounds.height -= shadow * 2; |
| return bounds; |
| } |
| |
| public void clear() { |
| myImage = null; |
| myAlpha = -1; |
| } |
| |
| @Override |
| public void doLayout() { |
| Insets insets = getInsets(); |
| if (insets == null) { |
| insets = new Insets(0, 0, 0, 0); |
| } |
| |
| myContent.setBounds(insets.left, insets.top, getWidth() - insets.left - insets.right, getHeight() - insets.top - insets.bottom); |
| } |
| |
| @Override |
| public Dimension getPreferredSize() { |
| return addInsets(myContent.getPreferredSize()); |
| } |
| |
| @Override |
| public Dimension getMinimumSize() { |
| return addInsets(myContent.getMinimumSize()); |
| } |
| |
| private Dimension addInsets(Dimension size) { |
| final Insets insets = getInsets(); |
| if (insets != null) { |
| size.width += insets.left + insets.right; |
| size.height += insets.top + insets.bottom; |
| } |
| |
| return size; |
| } |
| |
| @Override |
| protected void paintComponent(final Graphics g) { |
| super.paintComponent(g); |
| |
| final Graphics2D g2d = (Graphics2D)g; |
| |
| Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this); |
| |
| Rectangle shapeBounds = myContent.getBounds(); |
| |
| final int shadowSize = myBalloon.getShadowBorderSize(); |
| if (shadowSize > 0) { |
| if (myShadow == null) { |
| initComponentImage(pointTarget, shapeBounds); |
| myShadow = ShadowBorderPainter.createShadow(myImage, 0, 0, false, shadowSize / 2); |
| } |
| } |
| |
| if (myImage == null && myAlpha != -1) { |
| initComponentImage(pointTarget, shapeBounds); |
| } |
| |
| if (myImage != null && myAlpha != -1) { |
| g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, myAlpha)); |
| |
| UIUtil.drawImage(g2d, myImage, 0, 0, null); |
| } |
| else { |
| if (myShadow != null) { |
| UIUtil.drawImage(g, myShadow.getImage(), myShadow.getX(), myShadow.getY(), null); |
| } |
| myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)g, pointTarget); |
| } |
| } |
| |
| @Override |
| public boolean contains(int x, int y) { |
| Point pointTarget = SwingUtilities.convertPoint(myLayeredPane, myBalloon.myTargetPoint, this); |
| Rectangle bounds = myContent.getBounds(); |
| Shape shape; |
| if (myShowPointer) { |
| shape = myBalloon.myPosition.getPointingShape(bounds, pointTarget, myBalloon); |
| } |
| else { |
| shape = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width - 1, bounds.height - 1, myBalloon.getArc(), myBalloon.getArc()); |
| } |
| return shape.contains(x, y); |
| } |
| |
| private void initComponentImage(Point pointTarget, Rectangle shapeBounds) { |
| if (myImage != null) return; |
| |
| //noinspection UndesirableClassUsage |
| myImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); //[kb]: don't use UIUtil.createImage here |
| myBalloon.myPosition.paintComponent(myBalloon, shapeBounds, (Graphics2D)myImage.getGraphics(), pointTarget); |
| } |
| |
| |
| @Override |
| public void removeNotify() { |
| super.removeNotify(); |
| |
| if (!ScreenUtil.isStandardAddRemoveNotify(this)) { |
| return; |
| } |
| |
| final CloseButton closeButton = myCloseRec; |
| //noinspection SSBasedInspection |
| SwingUtilities.invokeLater(new Runnable() { |
| @Override |
| public void run() { |
| disposeCloseButton(closeButton); |
| } |
| }); |
| } |
| |
| public void setAlpha(float alpha) { |
| myAlpha = alpha; |
| paintImmediately(0, 0, getWidth(), getHeight()); |
| } |
| |
| public void _setBounds(Rectangle bounds) { |
| Rectangle currentBounds = getBounds(); |
| if (currentBounds.width != bounds.width || currentBounds.height != bounds.height) { |
| invalidateShadowImage(); |
| } |
| |
| super.setBounds(bounds); |
| if (myCloseRec.getParent() == null && getParent() != null) { |
| myLayeredPane.add(myCloseRec); |
| myLayeredPane.setLayer(myCloseRec, JLayeredPane.DRAG_LAYER); |
| } |
| |
| if (isVisible() && myCloseRec.isVisible()) { |
| Rectangle lpBounds = SwingUtilities.convertRectangle(getParent(), bounds, myLayeredPane); |
| lpBounds = myPosition.getPointlessContentRec(lpBounds, myBalloon.getPointerLength(myPosition)); |
| |
| int iconWidth = AllIcons.General.BalloonClose.getIconWidth(); |
| int iconHeight = AllIcons.General.BalloonClose.getIconHeight(); |
| Rectangle r = new Rectangle(lpBounds.x + lpBounds.width - iconWidth + (int)(iconWidth * 0.3), lpBounds.y - (int)(iconHeight * 0.3), iconWidth, iconHeight); |
| |
| r.y -= getShadowBorderSize(); |
| r.x -= getShadowBorderSize(); |
| |
| myCloseRec.setBounds(r); |
| } |
| |
| if (isVisible()) { |
| revalidate(); |
| repaint(); |
| } |
| } |
| |
| private void invalidateShadowImage() { |
| myImage = null; |
| myShadow = null; |
| } |
| |
| public void repaintButton() { |
| myCloseRec.repaint(); |
| } |
| } |
| |
| private static class Shaper { |
| private final GeneralPath myPath = new GeneralPath(); |
| |
| Rectangle myBounds; |
| @JdkConstants.TabPlacement |
| private final int myTargetSide; |
| private final BalloonImpl myBalloon; |
| |
| public Shaper(BalloonImpl balloon, Rectangle bounds, Point targetPoint, @JdkConstants.TabPlacement int targetSide) { |
| myBalloon = balloon; |
| myBounds = bounds; |
| myTargetSide = targetSide; |
| start(targetPoint); |
| } |
| |
| private void start(Point start) { |
| myPath.moveTo(start.x, start.y); |
| } |
| |
| public Shaper roundUpRight() { |
| myPath.quadTo(getCurrent().x, getCurrent().y - myBalloon.getArc(), getCurrent().x + myBalloon.getArc(), |
| getCurrent().y - myBalloon.getArc()); |
| return this; |
| } |
| |
| public Shaper roundRightDown() { |
| myPath.quadTo(getCurrent().x + myBalloon.getArc(), getCurrent().y, getCurrent().x + myBalloon.getArc(), |
| getCurrent().y + myBalloon.getArc()); |
| return this; |
| } |
| |
| public Shaper roundLeftUp() { |
| myPath.quadTo(getCurrent().x - myBalloon.getArc(), getCurrent().y, getCurrent().x - myBalloon.getArc(), |
| getCurrent().y - myBalloon.getArc()); |
| return this; |
| } |
| |
| public Shaper roundLeftDown() { |
| myPath.quadTo(getCurrent().x, getCurrent().y + myBalloon.getArc(), getCurrent().x - myBalloon.getArc(), |
| getCurrent().y + myBalloon.getArc()); |
| return this; |
| } |
| |
| public Point getCurrent() { |
| return new Point((int)myPath.getCurrentPoint().getX(), (int)myPath.getCurrentPoint().getY()); |
| } |
| |
| public Shaper line(final int deltaX, final int deltaY) { |
| myPath.lineTo(getCurrent().x + deltaX, getCurrent().y + deltaY); |
| return this; |
| } |
| |
| public Shaper lineTo(final int x, final int y) { |
| myPath.lineTo(x, y); |
| return this; |
| } |
| |
| private int getTargetDelta(@JdkConstants.TabPlacement int effectiveSide) { |
| return effectiveSide == myTargetSide ? myBalloon.getPointerLength(myBalloon.myPosition) : 0; |
| } |
| |
| public Shaper toRightCurve() { |
| myPath.lineTo((int)myBounds.getMaxX() - myBalloon.getArc() - getTargetDelta(SwingConstants.RIGHT) - 1, getCurrent().y); |
| return this; |
| } |
| |
| public Shaper toBottomCurve() { |
| myPath.lineTo(getCurrent().x, (int)myBounds.getMaxY() - myBalloon.getArc() - getTargetDelta(SwingConstants.BOTTOM) - 1); |
| return this; |
| } |
| |
| public Shaper toLeftCurve() { |
| myPath.lineTo((int)myBounds.getX() + myBalloon.getArc() + getTargetDelta(SwingConstants.LEFT), getCurrent().y); |
| return this; |
| } |
| |
| public Shaper toTopCurve() { |
| myPath.lineTo(getCurrent().x, (int)myBounds.getY() + myBalloon.getArc() + getTargetDelta(SwingConstants.TOP)); |
| return this; |
| } |
| |
| public void close() { |
| myPath.closePath(); |
| } |
| |
| public Shape getShape() { |
| return myPath; |
| } |
| } |
| |
| @Override |
| public boolean wasFadedIn() { |
| return myFadedIn; |
| } |
| |
| @Override |
| public boolean wasFadedOut() { |
| return myFadedOut; |
| } |
| |
| @Override |
| public boolean isDisposed() { |
| return myDisposed; |
| } |
| |
| @Override |
| public void setTitle(String title) { |
| myTitleLabel.setText(title); |
| } |
| |
| @Override |
| public RelativePoint getShowingPoint() { |
| Point p = myPosition.getShiftedPoint(myTargetPoint, myCalloutShift * -1); |
| return new RelativePoint(myLayeredPane, p); |
| } |
| |
| @Override |
| public void setAnimationEnabled(boolean enabled) { |
| myAnimationEnabled = enabled; |
| } |
| |
| public boolean isAnimationEnabled() { |
| return myAnimationEnabled; |
| } |
| |
| public boolean isBlockClicks() { |
| return myBlockClicks; |
| } |
| } |