package com.explodingpixels.widgets; import java.awt.Graphics; import java.awt.Insets; import java.awt.Rectangle; import java.awt.image.BufferedImage; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.plaf.basic.BasicButtonUI; /** * A button backed by an image. Additionally, a click mask can be provided. Any fully * non-transparent pixels in the mask will not be clickable. */ public class ImageButton extends JButton { // create a static index for the alpha channel of a raster image. i'm not exactly sure where // it's specified that red = channel 0, green = channel 1, blue = channel 2, and // alpha = channel 3, but this have been the values i've observed. private static final int ALPHA_BAND = 3; // a buffered image representing the mask for this button. private final BufferedImage fMask; private Icon fInactiveIcon; /** * Creates an image based button. * * @param icon the icon to use for the button. */ public ImageButton(Icon icon) { this(icon, icon); } /** * Creates an image based button with the given click mask. * * @param icon the icon to use for the button. * @param mask the click mask to use for the button. * @throws IllegalArgumentException if the given icon is null, the given mask is null or * the given mask's bounds do not match the given icons bounds. */ public ImageButton(Icon icon, Icon mask) { super(icon); if (icon == null) { throw new IllegalArgumentException("The icon cannot be null."); } if (mask == null) { throw new IllegalArgumentException("The mask cannot be null."); } checkIconMatchesMaskBounds(icon, mask); // remove the margins from this button, request that the content area not be filled, and // indicate that the border not be painted. setMargin(new Insets(0, 0, 0, 0)); setBorder(BorderFactory.createEmptyBorder()); setContentAreaFilled(false); // create the mask from the supplied icon. fMask = createMask(mask); // repaint this button when the parent window's focus state changes so // that we can correctly show the active or inactive icon. WindowUtils.installJComponentRepainterOnWindowFocusChanged(this); } private BufferedImage createMask(Icon mask) { // create a BufferedImage to paint the mask into so that we can later retrieve pixel data // out of the image. BufferedImage image = new BufferedImage( mask.getIconWidth(), mask.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics graphics = image.getGraphics(); mask.paintIcon(null, graphics, 0, 0); graphics.dispose(); return image; } public Icon getIcon() { return WindowUtils.isParentWindowFocused(this) || fInactiveIcon == null ? super.getIcon() : fInactiveIcon; } @Override public void setIcon(Icon defaultIcon) { super.setIcon(defaultIcon); // if this class has already been initialized, ensure that the new icon matches the bounds // of the current mask. if (fMask != null) { checkIconMatchesMaskBounds(defaultIcon, new ImageIcon(fMask)); } } public void setInactiveIcon(Icon inactiveIcon) { checkIconMatchesMaskBounds(inactiveIcon, new ImageIcon(fMask)); fInactiveIcon = inactiveIcon; } @Override public void updateUI() { // install the custom ui delegate to track the icon rectangle and answer the contains // method. setUI(new CustomButtonUI()); } private static void checkIconMatchesMaskBounds(Icon icon, Icon mask) { if (mask.getIconWidth() != icon.getIconWidth() || mask.getIconHeight() != icon.getIconHeight()) { throw new IllegalArgumentException("The mask must be the same size as the icon."); } } // CustomButtonUI implementation so that we can maintain the icon rectangle. ////////////////// private class CustomButtonUI extends BasicButtonUI { private Rectangle fIconRect; private boolean maskContains(int x, int y) { return fIconRect != null && fIconRect.contains(x, y) && fMask.getRaster().getSample(x - fIconRect.x, y - fIconRect.y, ALPHA_BAND) > 0; } @Override public boolean contains(JComponent c, int x, int y) { return maskContains(x, y); } @Override protected void paintIcon(Graphics g, JComponent c, Rectangle iconRect) { super.paintIcon(g, c, iconRect); // capture where the icon is being painted within the bounds of this button so we can // later use this information in the contains calculation. if (fIconRect == null || !fIconRect.equals(iconRect)) { // create a copy of the icon rectangle, as the given iconRect is a static variable // in BasicButtonUI that will be updated for each button painted. fIconRect = new Rectangle(iconRect); } } } }