//package bms.ui.widgets; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.Insets; import java.awt.Point; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Toolkit; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Area; import java.awt.geom.RoundRectangle2D; import javax.swing.AbstractAction; import javax.swing.JLabel; import javax.swing.JWindow; import javax.swing.Popup; import javax.swing.SwingUtilities; import javax.swing.Timer; import com.sun.jna.examples.WindowUtils; /** * The BalloonTipManager class handles creation and disposal of balloon style * tips that are typically used to display warning/error information based on * results from input validation. The balloon tip location and direction are * computed based on the owner component location on the screen. The balloon * tip is only displayed for a limited amount of time that is configurable. * The balloon tip is also disposed when the owner component loses focus or the * mouse is pressed. */ public class BalloonTipManager { public static final Color DEFAULT_BORDER_COLOR = Color.BLACK; public static final Color DEFAULT_BACKGROUND_COLOR = new Color(255, 255, 225); public static final Color DEFAULT_TEXT_COLOR = Color.BLACK; private static final Integer VPOS_ABOVE = 0; // Positioned above component. private static final Integer VPOS_BELOW = 1; // Positioned below component. private static final Integer HPOS_LEFT = 0; // Arrow is on the left side. private static final Integer HPOS_RIGHT = 1; // Arros is on the right side. private static Integer vpos = null; private static Integer hpos = null; private static Timer hidePopupTimer = null; private static boolean isShowing = false; /* * The BalloonTip class defines the look of the BalloonTip object. */ @SuppressWarnings("serial") private static final class BalloonTip extends JWindow { private static final Integer HMARGIN = 10; private static final Integer VMARGIN = 6; private static final Integer VSPACER = 4; private static final int ARC_D = 16; private Area mask = null; private Dimension maskSize = null; private String[] textList = null; private Color backgroundColor = null; private Color borderColor = null; private Color textColor = null; /** * Create a BalloonTip object. * @param owner the parent window for the components * @param content the string for the balloon tip * @param position the position for the balloon; either above or below the * owner component * @param origin the origin point for the balloon tip * @param bordercolor the background color for the balloon tip * @param backgroundcolor the border color for the balloon tip * @param textcolor the text color for the balloon tip */ public BalloonTip ( Window owner, String content, Point origin, Color bordercolor, Color backgroundcolor, Color textcolor) { super(owner); textList = content.split("\n"); borderColor = bordercolor; backgroundColor = backgroundcolor; textColor = textcolor; setFocusableWindowState(false); setName("###overrideRedirect###"); setSize(getPreferredSize()); } /* * Sets the mask for the Balloon Tip. */ private void setWindowMask () { mask = new Area(getMask()); maskSize = getSize(); WindowUtils.setWindowMask(BalloonTip.this, mask); } /* * (non-Javadoc) * @see java.awt.Container#paint(java.awt.Graphics) */ public void paint (Graphics g) { super.paint(g); Dimension d = getMinimumWindowSize(); int width = d.width + 2 * HMARGIN; int height = d.height + 2 * VMARGIN; int x = 0; int y = 0; if (vpos == VPOS_BELOW) { y += 15; } // Draw the filled rounded rectangle and clean up the missed pixels. g.setColor(backgroundColor); g.fillRoundRect(x, y, width, height, ARC_D, ARC_D); g.drawLine(x + 6, y + height - 1, x + 6, y + height - 1); g.drawLine(x + width - 1, y + 6, x + width - 1, y + 6); g.drawLine(x + width - 1, y + height - 6, x + width - 1, y + height - 7); g.drawLine(x + width - 2, y + height - 4, x + width - 4, y + height - 2); g.drawLine(x + width - 6, y + height - 1, x + width - 7, y + height - 1); g.clearRect(x + 2, y + 2, 1, 1); // Draw the border of the rounded rectangle. g.setColor(borderColor); g.drawRoundRect(x, y, width, height, ARC_D, ARC_D); // Draw the external triangle for the balloon. if (vpos == VPOS_BELOW) { if (hpos == HPOS_LEFT) { g.setColor(backgroundColor); int[] xPts = {16, 16, 31}; int[] yPts = {0, 16, 16}; g.fillPolygon(xPts, yPts, 3); g.setColor(borderColor); g.drawLine(16, 0, 16, 15); g.drawLine(16, 0, 31, 15); g.drawLine(16, 1, 30, 15); } else { g.setColor(backgroundColor); int[] xPts = {width - 16, width - 16, width - 31}; int[] yPts = {0, 16, 16}; g.fillPolygon(xPts, yPts, 3); g.setColor(borderColor); g.drawLine(width - 16, 0, width - 16, 15); g.drawLine(width - 16, 0, width - 31, 15); g.drawLine(width - 16, 1, width - 30, 15); } } else { if (hpos == HPOS_LEFT) { g.setColor(backgroundColor); int[] xPts = {16, 16, 31}; int[] yPts = {height, height + 16, height}; g.fillPolygon(xPts, yPts, 3); g.setColor(borderColor); g.drawLine(16, height, 16, height + 15); g.drawLine(16, height + 15, 31, height); g.drawLine(16, height + 14, 30, height); } else { g.setColor(backgroundColor); int[] xPts = {width - 16, width - 16, width - 31}; int[] yPts = {height, height + 16, height}; g.fillPolygon(xPts, yPts, 3); g.setColor(borderColor); g.drawLine(width - 16, height, width - 16, height + 15); g.drawLine(width - 16, height + 15, width - 31, height); g.drawLine(width - 16, height + 14, width - 30, height); } } // Draw the inner component for the balloon. g.setColor(textColor); g.setFont(new Font("Tahoma", Font.PLAIN, 11)); int stringY = y + VMARGIN / 2; for (int i = 0; i < textList.length; i++) { stringY += new JLabel(textList[i]).getPreferredSize().height; if (i > 0) { stringY += VSPACER; } g.drawString(textList[i], HMARGIN, stringY); } } /* * Returns the mask for the balloon tip window. */ private Shape getMask () { Dimension d = getMinimumWindowSize(); int width = d.width + 2 * HMARGIN; int height = d.height + 2 * VMARGIN; int x = 0; int y = 0; if (vpos == VPOS_BELOW) { y += 15; } // Start by creating the area of the main rounded rectangle. Area area = new Area( new RoundRectangle2D.Float(x, y, width + 1, height + 1, ARC_D, ARC_D)); // Add in the remaining pixels that are not included by default due to // the differences between Graphics drawing and Shape creation. area.add(new Area(new Rectangle(0, y + 6, 1, 2))); area.add(new Area(new Rectangle(0, y + height - 7, 1, 2))); area.add(new Area(new Rectangle(1, y + 4, 1, 1))); area.add(new Area(new Rectangle(1, y + height - 5, 1, 2))); area.add(new Area(new Rectangle(4, y + 1, 1, 1))); area.add(new Area(new Rectangle(6, y, 2, 1))); area.add(new Area(new Rectangle(width - 7, y, 2, 1))); // Subtract the extra pixels that are not included by default due to the // differences between Graphics drawing and Shape creation. area.subtract(new Area(new Rectangle(2, y + height - 2, 1, 1))); area.subtract(new Area(new Rectangle(3, y + height - 1, 1, 1))); area.subtract(new Area(new Rectangle(5, y + height, 1, 1))); area.subtract(new Area(new Rectangle(width - 5, y + height, 2, 1))); area.subtract(new Area(new Rectangle(width - 3, y + height - 1, 2, 1))); area.subtract(new Area(new Rectangle(width - 2, y + 2, 1, 1))); area.subtract(new Area(new Rectangle(width - 2, y + height - 2, 2, 1))); area.subtract(new Area(new Rectangle(width - 1, y + 3, 1, 1))); area.subtract(new Area(new Rectangle(width - 1, y + height - 3, 1, 1))); area.subtract(new Area(new Rectangle(width, y + 5, 1, 1))); area.subtract(new Area(new Rectangle(width, y + height - 5, 1, 2))); // Add in the triangle piece for the balloon. if (vpos == VPOS_BELOW) { if (hpos == HPOS_LEFT) { int[] xPts = {16, 16, 32}; int[] yPts = {-1, 16, 16}; area.add(new Area(new Polygon(xPts, yPts, 3))); } else { int[] xPts = {width - 15, width - 15, width - 32}; int[] yPts = {-1, 16, 16}; area.add(new Area(new Polygon(xPts, yPts, 3))); } } else { if (hpos == HPOS_LEFT) { int[] xPts = {16, 16, 31}; int[] yPts = {height, height + 16, height}; area.add(new Area(new Polygon(xPts, yPts, 3))); } else { int[] xPts = {width - 15, width - 15, width - 32}; int[] yPts = {height, height + 16, height}; area.add(new Area(new Polygon(xPts, yPts, 3))); } } return area; } /* * Returns the dimension of the window based on the preferred component * sizes. */ private Dimension getMinimumWindowSize () { int maxWidth = 0; int textHeight = 0; JLabel tempLabel = null; for (int i = 0; i < textList.length; i++) { tempLabel = new JLabel(textList[i]); maxWidth = Math.max(maxWidth, tempLabel.getPreferredSize().width); textHeight += tempLabel.getPreferredSize().height; } int w = Math.max(maxWidth, 32); int h = Math.max(textHeight + (textList.length - 1) * VSPACER, 8); return new Dimension(w, h); } /* * (non-Javadoc) * @see java.awt.Window#setBounds(int, int, int, int) */ public void setBounds (int x, int y, int w, int h) { super.setBounds(x, y, w, h); Dimension size = new Dimension(w, h); if (mask != null && !size.equals(maskSize)) { mask.subtract(mask); mask.add(new Area(getMask())); maskSize = size; } } /* * (non-Javadoc) * @see java.awt.Container#getPreferredSize() */ public Dimension getPreferredSize () { Dimension d = getMinimumWindowSize(); int w = d.width + 2 * HMARGIN + 1; int h = d.height + 2 * VMARGIN + 16; return new Dimension(w, h); } } /** * Returns the popup window of the balloon tip. * @param owner the owner component for the balloon tip * @param content the text string to display in the balloon tip * @param x the x coordinate for the origin for the balloon tip in relation * to the owner component * @param y the y coordinate for the origin for the balloon tip in relation * to the owner component * @param position the position for the balloon; either above or below the * owner component * @param duration the duration in milliseconds to display balloon tip * @return the popup window of the balloon tip */ public static Popup getBalloonTip (final Component owner, final String content, int x, int y, final int duration) { return getBalloonTip(owner, content, x, y, duration, DEFAULT_BORDER_COLOR, DEFAULT_BACKGROUND_COLOR, DEFAULT_TEXT_COLOR); } /** * Returns whether the popup is showing or not. * @return true if the popup is showing, else false */ public static boolean isShowing () { return isShowing; } /** * Restarts the popup timer. */ public static void restartTimer () { hidePopupTimer.restart(); } /** * Returns the popup window of the balloon tip. * @param owner the owner component for the balloon tip * @param content the text string to display in the balloon tip * @param x the x coordinate for the origin for the balloon tip in relation * to the owner component * @param y the y coordinate for the origin for the balloon tip in relation * to the owner component * @param position the position for the balloon; either above or below the * owner component * @param duration the duration in milliseconds to display balloon tip * @param bordercolor the background color for the balloon tip * @param backgroundcolor the border color for the balloon tip * @param textcolor the text color for the balloon tip * @return the popup window of the balloon tip */ public static Popup getBalloonTip (final Component owner, final String content, int x, int y, final Integer duration, final Color bordercolor, final Color backgroundcolor, final Color textcolor) { final Point origin = owner == null ? new Point(0, 0) : owner.getLocationOnScreen(); final Window parent = owner != null ? SwingUtilities.getWindowAncestor(owner) : null; final String text = content != null ? content : ""; final Integer timerDuration = duration != null ? duration : 10000; origin.translate(x, y); vpos = VPOS_BELOW; hpos = HPOS_LEFT; return new Popup () { private BalloonTip bt = null; final ComponentEar componentEar = new ComponentEar(); final MouseEar mouseEar = new MouseEar(); final FocusEar focusEar = new FocusEar(); /* * (non-Javadoc) * @see javax.swing.Popup#show() */ public void show () { hidePopupTimer = new Timer(timerDuration, new TimerAction()); bt = new BalloonTip(parent, text, origin, bordercolor, backgroundcolor, textcolor); bt.pack(); Point pt = new Point(origin); pt.translate(10, owner.getHeight()); bt.setLocation(getAdjustedOrigin(pt)); bt.setWindowMask(); bt.setVisible(true); owner.addFocusListener(focusEar); owner.addMouseListener(mouseEar); parent.addMouseListener(mouseEar); parent.addComponentListener(componentEar); hidePopupTimer.start(); isShowing = true; } /* * (non-Javadoc) * @see javax.swing.Popup#hide() */ public void hide () { if (bt != null) { isShowing = false; hidePopupTimer.stop(); parent.removeComponentListener(componentEar); parent.removeMouseListener(mouseEar); owner.removeMouseListener(mouseEar); owner.removeFocusListener(focusEar); bt.setVisible(false); bt.dispose(); } } /* * Adjust the location of the balloon popup so that is drawn completely * on the screen and specify the orientation. */ private Point getAdjustedOrigin (Point pt) { Point ret = new Point(pt.x, pt.y); GraphicsConfiguration gc = owner.getGraphicsConfiguration(); Rectangle sBounds = gc.getBounds(); Insets sInsets = Toolkit.getDefaultToolkit().getScreenInsets(gc); sBounds.x += sInsets.left; sBounds.y += sInsets.top; sBounds.width -= (sInsets.left + sInsets.right); sBounds.height -= (sInsets.top + sInsets.bottom); if (ret.x < sBounds.x) { ret.x = sBounds.x; } else if (ret.x - sBounds.x + bt.getWidth() > sBounds.width) { ret.x = owner.getLocationOnScreen().x - bt.getWidth() + 43; } if (ret.x >= pt.x) { hpos = HPOS_LEFT; } else { hpos = HPOS_RIGHT; } if (ret.y < sBounds.y) { ret.y = sBounds.y; } else if (ret.y - sBounds.y + bt.getHeight() > sBounds.height) { ret.y = owner.getLocationOnScreen().y - bt.getHeight(); } if (ret.y >= pt.y) { vpos = VPOS_BELOW; } else { vpos = VPOS_ABOVE; } return ret; } /* * This class handles actions from the balloon tip timer. */ @SuppressWarnings("serial") final class TimerAction extends AbstractAction { /* * (non-Javadoc) * @see java.awt.event.ActionListener#actionPerformed( * java.awt.event.ActionEvent) */ public void actionPerformed (ActionEvent e) { hide(); } } /* * This class handles events spawned from moving the component. */ @SuppressWarnings("serial") final class ComponentEar extends ComponentAdapter { /* * (non-Javadoc) * @see java.awt.event.ComponentAdapter#componentMoved( * java.awt.event.ComponentEvent) */ public void componentMoved (ComponentEvent e) { hide(); } } /* * This class handles events spawned when a mouse button is pressed. */ @SuppressWarnings("serial") final class MouseEar extends MouseAdapter { /* * (non-Javadoc) * @see java.awt.event.MouseAdapter#mousePressed( * java.awt.event.MouseEvent) */ public void mousePressed (MouseEvent e) { hide(); } } /* * This class handles events spawned when the component loses focus. */ @SuppressWarnings("serial") final class FocusEar extends FocusAdapter { /* * (non-Javadoc) * @see java.awt.event.FocusAdapter#focusLost( * java.awt.event.FocusEvent) */ public void focusLost (FocusEvent e) { hide(); } } }; } }