package edu.princeton.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; /** * PHyperlink is a subclass of AbstractButton which looks and feels like a hyperlink. Note the * following correlation between the properties of a ButtonModel and the state of the hyperlink:
* !isPressed() && !isRollover() && !isSelected -> Link
* !isPressed() && isRollover() && !isSelected -> Hover
* !isPressed() && !isRollover() && isSelected -> Visited
* !isPressed() && isRollover() && isSelected -> Visited Hover
* isPressed() -> Active
* * @author btsang * @version 7.1 */ public class PHyperlink extends AbstractButton { public static final byte CLASSIC_STYLE = (byte)0; public static final byte CLASSIC_HOVER_STYLE = (byte)1; public static final byte BLUE_STYLE = (byte)2; public static final byte METAL_STYLE = (byte)3; private static final Color STYLE_COLORS[][] = { { // Classic new Color( 0, 0, 255), new Color( 0, 0, 255), new Color(128, 128, 128), new Color(128, 0, 128), new Color(128, 0, 128), new Color(128, 128, 128), new Color(255, 0, 0) }, { // Classic w/ hover new Color( 0, 0, 255), new Color( 0, 0, 255), new Color(128, 128, 128), new Color(128, 0, 128), new Color(128, 0, 128), new Color(128, 128, 128), new Color(255, 0, 0) }, { // Blue new Color( 0, 0, 255), new Color( 0, 0, 255), new Color(128, 128, 128), new Color( 0, 0, 255), new Color( 0, 0, 255), new Color(128, 128, 128), new Color( 73, 77, 255) }, { // Metal new Color(102, 102, 153), new Color(102, 102, 153), new Color(128, 128, 128), new Color(102, 102, 153), new Color(102, 102, 153), new Color(128, 128, 128), new Color(145, 146, 183) } }; private static final boolean STYLE_UNDERLINES[][] = { { // Classic true, true, false, true, true, false, true }, { // Classic w/ hover false, true, false, false, true, false, true }, { // Blue false, true, false, false, true, false, true }, { // Metal false, true, false, false, true, false, true } }; private static final Border EMPTY_BORDER = new EmptyBorder(0, 0, 1, 0); private static final Border UNDERLINE_BORDER = new UnderlineBorder(null, 0, 1); private JLabel label; private ButtonModel model; private Listener listener; private Color linkColor; private boolean linkUnderline; private Color hoverColor; private boolean hoverUnderline; private Color disabledColor; private boolean disabledUnderline; private Color visitedColor; private boolean visitedUnderline; private Color visitedHoverColor; private boolean visitedHoverUnderline; private Color visitedDisabledColor; private boolean visitedDisabledUnderline; private Color activeColor; private boolean activeUnderline; /** * Create a new PHyperlink. */ public PHyperlink() { this( "", null, Color.blue, false, Color.blue, true, Color.gray, false, new Color(128, 0, 128), false, new Color(128, 0, 128), true, Color.gray, false, Color.red, true ); } /** * Create a new PHyperlink. */ public PHyperlink(String text) { this( text, null, Color.blue, false, Color.blue, true, Color.gray, false, new Color(128, 0, 128), false, new Color(128, 0, 128), true, Color.gray, false, Color.red, true ); } /** * Create a new PHyperlink. */ public PHyperlink(String text, Icon icon) { this( text, icon, Color.blue, false, Color.blue, true, Color.gray, false, new Color(128, 0, 128), false, new Color(128, 0, 128), true, Color.gray, false, Color.red, true ); } /** * Create a new PHyperlink. */ public PHyperlink(String text, Icon icon, byte style) { this( text, icon, STYLE_COLORS[style][0], STYLE_UNDERLINES[style][0], STYLE_COLORS[style][1], STYLE_UNDERLINES[style][1], STYLE_COLORS[style][2], STYLE_UNDERLINES[style][2], STYLE_COLORS[style][3], STYLE_UNDERLINES[style][3], STYLE_COLORS[style][4], STYLE_UNDERLINES[style][4], STYLE_COLORS[style][5], STYLE_UNDERLINES[style][5], STYLE_COLORS[style][6], STYLE_UNDERLINES[style][6] ); } /** * Create a new PHyperlink. */ public PHyperlink(Action a) { this((String)a.getValue(Action.NAME), (Icon)a.getValue(Action.SMALL_ICON)); setAction(a); } /** * Create a new PHyperlink. */ public PHyperlink(Action a, byte style) { this( (String)a.getValue(Action.NAME), (Icon)a.getValue(Action.SMALL_ICON), STYLE_COLORS[style][0], STYLE_UNDERLINES[style][0], STYLE_COLORS[style][1], STYLE_UNDERLINES[style][1], STYLE_COLORS[style][2], STYLE_UNDERLINES[style][2], STYLE_COLORS[style][3], STYLE_UNDERLINES[style][3], STYLE_COLORS[style][4], STYLE_UNDERLINES[style][4], STYLE_COLORS[style][5], STYLE_UNDERLINES[style][5], STYLE_COLORS[style][6], STYLE_UNDERLINES[style][6] ); setAction(a); } /** * Create a new PHyperlink. */ public PHyperlink(String text, Icon icon, Color linkColor, boolean linkUnderline, Color hoverColor, boolean hoverUnderline, Color disabledColor, boolean disabledUnderline, Color visitedColor, boolean visitedUnderline, Color visitedHoverColor, boolean visitedHoverUnderline, Color visitedDisabledColor, boolean visitedDisabledUnderline, Color activeColor, boolean activeUnderline) { initHyperlink(); setText(text); setIcon(icon); setLinkColor(linkColor); setLinkUnderline(linkUnderline); setHoverColor(hoverColor); setHoverUnderline(hoverUnderline); setDisabledColor(disabledColor); setDisabledUnderline(disabledUnderline); setVisitedColor(visitedColor); setVisitedUnderline(visitedUnderline); setVisitedHoverColor(visitedHoverColor); setVisitedHoverUnderline(visitedHoverUnderline); setVisitedDisabledColor(visitedDisabledColor); setVisitedDisabledUnderline(visitedDisabledUnderline); setActiveColor(activeColor); setActiveUnderline(activeUnderline); } /** * Called by the constructors to initialize the hyperlink. */ public void initHyperlink() { listener = new Listener(); setBackground(null); setLayout(new CardLayout()); label = new JLabel(getText(), getIcon(), JLabel.LEFT); label.setBackground(null); setFont(label.getFont().deriveFont(Font.PLAIN)); label.setFont(null); label.setBorder(EMPTY_BORDER); label.addMouseListener(listener); add(label, "label"); setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); setModel(new HyperlinkButtonModel()); } /** * Updates the label's properties to be consistent with this AbstractButton and its ButtonModel. */ private void updateLabel() { if (!model.isEnabled()) { if (model.isSelected()) { label.setForeground(visitedDisabledColor!=null?visitedDisabledColor:linkColor); label.setBorder(visitedDisabledUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getDisabledSelectedIcon()); } else { label.setForeground(disabledColor!=null?disabledColor:linkColor); label.setBorder(disabledUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getDisabledIcon()); } } if (model.isPressed()) { label.setForeground(activeColor!=null?activeColor:linkColor); label.setBorder(activeUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getPressedIcon()); } else if (model.isRollover()) { if (model.isSelected()) { label.setForeground(visitedHoverColor!=null?visitedHoverColor:linkColor); label.setBorder(visitedHoverUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getRolloverSelectedIcon()); } else { label.setForeground(hoverColor!=null?hoverColor:linkColor); label.setBorder(hoverUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getRolloverIcon()); } } else { if (model.isSelected()) { label.setForeground(visitedColor!=null?visitedColor:linkColor); label.setBorder(visitedUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getSelectedIcon()); } else { label.setForeground(linkColor); label.setBorder(linkUnderline?UNDERLINE_BORDER:EMPTY_BORDER); label.setIcon(getIcon()); } } } /** * Intercept setModel() calls to keep a listener on the model. */ public void setModel(ButtonModel model) { super.setModel(model); if (model != this.model && listener != null) { if (this.model != null) this.model.removeChangeListener(listener); this.model = model; model.addChangeListener(listener); updateLabel(); } } /** * Intercept setEnabled() calls to propagate the changes to the label. */ public void setEnabled(boolean enabled) { super.setEnabled(enabled); label.setEnabled(enabled); } /** * Intercept setText() calls to propagate the changes to the label. */ public void setText(String text) { super.setText(text); label.setText(text); } /** * Intercept setPressedIcon() calls to propagate the changes to the label. */ public void setPressedIcon(Icon icon) { super.setPressedIcon(icon); updateLabel(); } /** * Intercept setIcon() calls to propagate the changes to the label. */ public void setIcon(Icon icon) { super.setIcon(icon); updateLabel(); } /** * Intercept setSelectedIcon() calls to propagate the changes to the label. */ public void setSelectedIcon(Icon icon) { super.setSelectedIcon(icon); updateLabel(); } /** * Intercept setRolloverIcon() calls to propagate the changes to the label. */ public void setRolloverIcon(Icon icon) { super.setRolloverIcon(icon); updateLabel(); } /** * Intercept setRolloverSelectedIcon() calls to propagate the changes to the label. */ public void setRolloverSelectedIcon(Icon icon) { super.setRolloverSelectedIcon(icon); updateLabel(); } /** * Intercept setDisabledIcon() calls to propagate the changes to the label. */ public void setDisabledIcon(Icon icon) { super.setDisabledIcon(icon); updateLabel(); } /** * Intercept setDisabledSelectedIcon() calls to propagate the changes to the label. */ public void setDisabledSelectedIcon(Icon icon) { super.setDisabledSelectedIcon(icon); updateLabel(); } /** * Sets the color of the link in a non-hover, non-visited, non-active, non-disabled state. A * null value indicates that color should be the same as the foreground. * * @param color The new color of a the link in a non-hover, non-visited, non-active, * non-disabled state. */ public void setLinkColor(Color color) { if (color != linkColor) { Color oldValue = linkColor; linkColor = color; firePropertyChange("linkColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in a non-hover, non-visited, non-active, non-disabled state. * A null value indicates that color should be the same as the foreground. * * @return The color of a the link in a non-hover, non-visited, non-active, non-disabled state. */ public Color getLinkColor() { return linkColor; } /** * Sets wheter or not to underline the link in a non-hover, non-visited, non-active, * non-disabled state. * * @param underline Wheter or not to underline the link in a non-hover, non-visited, * non-active, non-disabled state. */ public void setLinkUnderline(boolean underline) { if (underline != linkUnderline) { boolean oldValue = linkUnderline; linkUnderline = underline; firePropertyChange("linkUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in a non-hover, non-visited, non-active, * non-disabled state. * * @return underline Wheter or not to underline the link in a non-hover, non-visited, * non-active, non-disabled state. */ public boolean getLinkUnderline() { return linkUnderline; } /** * Sets the color of the link in a hover, non-visited, non-active, non-disabled state. A * null value indicates that color should be the same as the link color. * * @param color The new color of a the link in a hover, non-visited, non-active, * non-disabled state. */ public void setHoverColor(Color color) { if (color != hoverColor) { Color oldValue = hoverColor; hoverColor = color; firePropertyChange("hoverColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in a hover, non-visited, non-active, non-disabled state. * A null value indicates that color should be the same as the link color. * * @return The color of a the link in a hover, non-visited, non-active, non-disabled state. */ public Color getHoverColor() { return hoverColor; } /** * Sets wheter or not to underline the link in a hover, non-visited, non-active, * non-disabled state. * * @param underline Wheter or not to underline the link in a hover, non-visited, * non-active, non-disabled state. */ public void setHoverUnderline(boolean underline) { if (underline != hoverUnderline) { boolean oldValue = hoverUnderline; hoverUnderline = underline; firePropertyChange("hoverUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in a hover, non-visited, non-active, * non-disabled state. * * @return underline Wheter or not to underline the link in a hover, non-visited, * non-active, non-disabled state. */ public boolean getHoverUnderline() { return hoverUnderline; } /** * Sets the color of the link in a non-visited, disabled state. A null value indicates that * color should be the same as the link color. * * @param color The new color of a the link in a non-visited, disabled state. */ public void setDisabledColor(Color color) { if (color != disabledColor) { Color oldValue = disabledColor; disabledColor = color; firePropertyChange("disabledColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in a non-visited, disabled state. A null value indicates that * color should be the same as the link color. * * @return The color of a the link in a non-visited, disabled state. */ public Color getDisabledColor() { return disabledColor; } /** * Sets wheter or not to underline the link in a non-visited, disabled state. * * @param underline Wheter or not to underline the link in a non-visited, disabled state. */ public void setDisabledUnderline(boolean underline) { if (underline != disabledUnderline) { boolean oldValue = disabledUnderline; disabledUnderline = underline; firePropertyChange("disabledUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in a non-visited, disabled state. * * @return underline Wheter or not to underline the link in a non-visited, disabled state. */ public boolean getDisabledUnderline() { return disabledUnderline; } /** * Sets the color of the link in a non-hover, visited, non-active, non-disabled state. A * null value indicates that color should be the same as the link color. * * @param color The new color of a the link in a non-hover, visited, non-active, * non-disabled state. */ public void setVisitedColor(Color color) { if (color != visitedColor) { Color oldValue = visitedColor; visitedColor = color; firePropertyChange("visitedColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in a non-hover, visited, non-active, non-disabled state. * A null value indicates that color should be the same as the link color. * * @return The color of a the link in a non-hover, visited, non-active, non-disabled state. */ public Color getVisitedColor() { return visitedColor; } /** * Sets wheter or not to underline the link in a non-hover, visited, non-active, * non-disabled state. * * @param underline Wheter or not to underline the link in a non-hover, visited, * non-active, non-disabled state. */ public void setVisitedUnderline(boolean underline) { if (underline != visitedUnderline) { boolean oldValue = visitedUnderline; visitedUnderline = underline; firePropertyChange("visitedUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in a non-hover, visited, non-active, * non-disabled state. * * @return underline Wheter or not to underline the link in a non-hover, visited, * non-active, non-disabled state. */ public boolean getVisitedUnderline() { return visitedUnderline; } /** * Sets the color of the link in a hover, visited, non-active, non-disabled state. A * null value indicates that color should be the same as the link color. * * @param color The new color of a the link in a hover, visited, non-active, * non-disabled state. */ public void setVisitedHoverColor(Color color) { if (color != visitedHoverColor) { Color oldValue = visitedHoverColor; visitedHoverColor = color; firePropertyChange("visitedHoverColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in a hover, visited, non-active, non-disabled state. * A null value indicates that color should be the same as the link color. * * @return The color of a the link in a hover, visited, non-active, non-disabled state. */ public Color getVisitedHoverColor() { return visitedHoverColor; } /** * Sets wheter or not to underline the link in a hover, visited, non-active, * non-disabled state. * * @param underline Wheter or not to underline the link in a hover, visited, * non-active, non-disabled state. */ public void setVisitedHoverUnderline(boolean underline) { if (underline != visitedHoverUnderline) { boolean oldValue = visitedHoverUnderline; visitedHoverUnderline = underline; firePropertyChange("visitedHoverUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in a hover, visited, non-active, * non-disabled state. * * @return underline Wheter or not to underline the link in a hover, visited, * non-active, non-disabled state. */ public boolean getVisitedHoverUnderline() { return visitedHoverUnderline; } /** * Sets the color of the link in a visited, disabled state. A null value indicates that color * should be the same as the link color. * * @param color The new color of a the link in a visited, disabled state. */ public void setVisitedDisabledColor(Color color) { if (color != visitedDisabledColor) { Color oldValue = visitedDisabledColor; visitedDisabledColor = color; firePropertyChange("visitedDisabledColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in a visited, disabled state. A null value indicates that * color should be the same as the link color. * * @return The color of a the link in a visited, disabled state. */ public Color getVisitedDisabledColor() { return visitedDisabledColor; } /** * Sets wheter or not to underline the link in a visited, disabled state. * * @param underline Wheter or not to underline the link in a visited, disabled state. */ public void setVisitedDisabledUnderline(boolean underline) { if (underline != visitedDisabledUnderline) { boolean oldValue = visitedDisabledUnderline; visitedDisabledUnderline = underline; firePropertyChange("visitedDisabledUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in a visited, disabled state. * * @return underline Wheter or not to underline the link in a visited, disabled state. */ public boolean getVisitedDisabledUnderline() { return visitedDisabledUnderline; } /** * Sets the color of the link in an active, non-disabled state. A null value indicates that * color should be the same as the link color. * * @param color The new color of a the link in an active, non-disabled state. */ public void setActiveColor(Color color) { if (color != activeColor) { Color oldValue = activeColor; activeColor = color; firePropertyChange("activeColor", oldValue, color); updateLabel(); } } /** * Returns the color of the link in an active, non-disabled state. A null value indicates that * color should be the same as the link color. * * @return The color of a the link in an active, non-disabled state. */ public Color getActiveColor() { return activeColor; } /** * Sets wheter or not to underline the link in an active, non-disabled state. * * @param underline Wheter or not to underline the link in an active, non-disabled state. */ public void setActiveUnderline(boolean underline) { if (underline != activeUnderline) { boolean oldValue = activeUnderline; activeUnderline = underline; firePropertyChange("activeUnderline", oldValue, underline); updateLabel(); } } /** * Returns wheter or not to underline the link in an active, non-disabled state. * * @return underline Wheter or not to underline the link in an active, non-disabled state. */ public boolean getActiveUnderline() { return activeUnderline; } /** * Listener implements ChangeListener to listen for changes in the ButtonModel's state and * MouseListener to listen for mouse actions on the label. */ protected class Listener implements ChangeListener, MouseListener { /** * Creates a new Listener object. */ protected Listener() { } /** * Implements ChangeListener to listen for changes in the ButtonModel's state. */ public void stateChanged(ChangeEvent e) { if (model.isEnabled() != isEnabled()) setEnabled(model.isEnabled()); else updateLabel(); } /** * Implement MouseListener to update the model's state based on mouse events. */ public void mouseClicked(MouseEvent e) { } /** * Implement MouseListener to update the model's state based on mouse events. */ public void mousePressed(MouseEvent e) { model.setArmed(true); model.setPressed(true); } /** * Implement MouseListener to update the model's state based on mouse events. */ public void mouseReleased(MouseEvent e) { model.setPressed(false); model.setArmed(false); } /** * Implement MouseListener to update the model's state based on mouse events. */ public void mouseEntered(MouseEvent e) { model.setArmed(model.isPressed()); model.setRollover(true); } /** * Implement MouseListener to update the model's state based on mouse events. */ public void mouseExited(MouseEvent e) { model.setRollover(false); model.setArmed(false); } } /** * HyperlinkButtonModel is similar to JToggleButton.ToggleButtonModel except that once it is * selected, it doesn't ever toggle back without an explicit setSelected() call. * * @author btsang * @version 7.1 */ public static class HyperlinkButtonModel extends DefaultButtonModel { /** * Construct a new HyperlinkButtonModel. */ public HyperlinkButtonModel() { super(); } /** * Override setPressed to intercept events which should change the selected state. */ public void setPressed(boolean b) { if (isPressed() != b && isEnabled()) { if (b == false && isArmed() && !isSelected()) setSelected(true); super.setPressed(b); } } } /** * UnderlineBorder is a simple subclass of AbstractBorder that draws an underline. * * @author btsang * @version 7.1 */ public static class UnderlineBorder extends AbstractBorder { private Color color; private int pad; private int thickness; /** * Creates a new UnderlineBorder. * * @param color The color of the border. If null is passed, the underline will be drawn * with the Component's foreground color. * @param pad The number of pixels of transparent space before the underline. Negative * values will result in an IllegalArgumentException. * @param thickness The thickness of the underline. Negative values will result in an * IllegalArgumentException. */ public UnderlineBorder(Color color, int pad, int thickness) { this.color = color; if (pad < 0) throw new IllegalArgumentException(); this.pad = pad; if (thickness < 0) throw new IllegalArgumentException(); this.thickness = thickness; } /** * Returns the color of the border. A null value indicates that the the underline will be * drawn with the Component's foreground color. * * @return The color of the border. */ public Color getColor() { return color; } /** * Returns the number of pixels of transparent space before the underline. * * @return The number of pixels of transparent space before the underline. */ public int getPad() { return pad; } /** * Returns the thickness of the underline. * * @return The thickness of the underline. */ public int getThickness() { return thickness; } /** * Paints the border for the specified component with the specified position and size. */ public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) { Color oldColor = g.getColor(); Color color = this.color; if (color == null) color = c.getForeground(); if (color == null) return; g.setColor(color); g.fillRect(x, y + height - thickness, width, thickness); g.setColor(oldColor); } /** * Returns the insets of the border. */ public Insets getBorderInsets(Component c) { return new Insets(0, 0, pad + thickness, 0); } /** * Reinitialize the insets parameter with this Border's current Insets. */ public Insets getBorderInsets(Component c, Insets insets) { insets.top = 0; insets.left = 0; insets.bottom = pad + thickness; insets.right = 0; return insets; } /** * Returns whether or not the border is opaque. */ public boolean isBorderOpaque() { return pad == 0; } } }