001/*
002 * $Id: JXHyperlink.java 4162 2012-02-08 16:21:35Z kschaefe $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 * 
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 * Lesser General Public License for more details.
016 * 
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020 */
021package org.jdesktop.swingx;
022
023import java.awt.Color;
024import java.awt.GraphicsEnvironment;
025import java.awt.HeadlessException;
026import java.awt.event.ActionEvent;
027import java.beans.PropertyChangeEvent;
028import java.beans.PropertyChangeListener;
029import java.net.URI;
030
031import javax.swing.Action;
032import javax.swing.JButton;
033import javax.swing.plaf.ButtonUI;
034
035import org.jdesktop.beans.JavaBean;
036import org.jdesktop.swingx.hyperlink.AbstractHyperlinkAction;
037import org.jdesktop.swingx.hyperlink.HyperlinkAction;
038import org.jdesktop.swingx.plaf.HyperlinkAddon;
039import org.jdesktop.swingx.plaf.LookAndFeelAddons;
040
041/**
042 * A hyperlink component that derives from JButton to provide compatibility
043 * mostly for binding actions enabled/disabled behavior accessibility i18n etc...
044 * <p>
045 *
046 * This button has visual state related to a notion of "clicked": 
047 * foreground color is unclickedColor or clickedColor depending on 
048 * its boolean bound property clicked being false or true, respectively.
049 * If the hyperlink has an action, it guarantees to synchronize its 
050 * "clicked" state to an action value with key LinkAction.VISITED_KEY. 
051 * Synchronization happens on setAction() and on propertyChange notification
052 * from the action. JXHyperlink accepts any type of action - 
053 * {@link AbstractHyperlinkAction} is a convenience implementation to
054 * simplify clicked control.
055 * <p>
056 * 
057 * <pre> <code>
058 *      LinkAction linkAction = new LinkAction("http://swinglabs.org") {
059 *            public void actionPerformed(ActionEvent e) {
060 *                doSomething(getTarget());
061 *                setVisited(true);
062 *            }
063 *      };
064 *      JXHyperlink hyperlink = new JXHyperlink(linkAction);
065 * <code> </pre>
066 * 
067 * The hyperlink can be configured to always update its clicked 
068 * property after firing the actionPerformed:
069 * 
070 * <pre> <code>
071 *      JXHyperlink hyperlink = new JXHyperlink(action);
072 *      hyperlink.setOverrulesActionOnClick(true);
073 * <code> </pre>
074 * 
075 * By default, this property is false. The hyperlink will 
076 * auto-click only if it has no action. Developers can change the
077 * behaviour by overriding {@link JXHyperlink#isAutoSetClicked()};
078 * 
079 * @author Richard Bair
080 * @author Shai Almog
081 * @author Jeanette Winzenburg
082 */
083@JavaBean
084public class JXHyperlink extends JButton {
085
086    /**
087     * @see #getUIClassID
088     * @see #readObject
089     */
090    public static final String uiClassID = "HyperlinkUI";
091
092    // ensure at least the default ui is registered
093    static {
094      LookAndFeelAddons.contribute(new HyperlinkAddon());
095    }
096
097    private boolean hasBeenVisited = false;
098
099    /**
100     * Color for the hyper link if it has not yet been clicked. This color can
101     * be set both in code, and through the UIManager with the property
102     * "JXHyperlink.unclickedColor".
103     */
104    private Color unclickedColor;
105
106    /**
107     * Color for the hyper link if it has already been clicked. This color can
108     * be set both in code, and through the UIManager with the property
109     * "JXHyperlink.clickedColor".
110     */
111    private Color clickedColor;
112
113    private boolean overrulesActionOnClick;
114
115    /**
116     * Creates a new instance of JXHyperlink with default parameters
117     */
118    public JXHyperlink() {
119        this(null);
120    }
121
122    /**
123     * Creates a new instance of JHyperLink and configures it from provided Action.
124     *
125     * @param action Action whose parameters will be borrowed to configure newly 
126     *        created JXHyperLink
127     */
128    public JXHyperlink(Action action) {
129        super();
130        setAction(action);
131        init();
132    }
133
134    /**
135     * Convenience method to create and install a HyperlinkAction for the given URI.
136     * 
137     * @param uri
138     *            to uri to create a HyperlinkAction for, maybe null.
139     * @throws HeadlessException
140     *             if {@link GraphicsEnvironment#isHeadless()} returns {@code true}
141     * @throws UnsupportedOperationException
142     *             if the current platform doesn't support Desktop
143     * 
144     * @see HyperlinkAction#createHyperlinkAction(URI)
145     */
146    public void setURI(URI uri) {
147        setAction(HyperlinkAction.createHyperlinkAction(uri));
148    }
149    
150    /**
151     * Returns the foreground color for unvisited links.
152     * 
153     * @return Color for the hyper link if it has not yet been clicked.
154     */
155    public Color getUnclickedColor() {
156        return unclickedColor;
157    }
158
159    /**
160     * Sets the color for the previously visited link. This value will override the one
161     * set by the "JXHyperlink.clickedColor" UIManager property and defaults.
162     *
163     * @param color Color for the hyper link if it has already been clicked.
164     */
165    public void setClickedColor(Color color) {
166        Color old = getClickedColor();
167        clickedColor = color;
168        if (isClicked()) {
169            setForeground(getClickedColor());
170        }
171        firePropertyChange("clickedColor", old, getClickedColor());
172    }
173
174    /**
175     * Returns the foreground color for visited links.
176     * 
177     * @return Color for the hyper link if it has already been clicked.
178     */
179    public Color getClickedColor() {
180        return clickedColor;
181    }
182
183    /**
184     * Sets the color for the previously not visited link. This value will override the one
185     * set by the "JXHyperlink.unclickedColor" UIManager property and defaults.
186     *
187     * @param color Color for the hyper link if it has not yet been clicked.
188     */
189    public void setUnclickedColor(Color color) {
190        Color old = getUnclickedColor();
191        unclickedColor = color;
192        if (!isClicked()) {
193            setForeground(getUnclickedColor());
194        }
195        firePropertyChange("unclickedColor", old, getUnclickedColor());
196    }
197
198    /**
199     * Sets the clicked property and updates visual state depending on clicked.
200     * This implementation updated the foreground color.
201     * <p>
202     * 
203     * NOTE: as with all button's visual properties, this will not update the
204     * backing action's "visited" state.
205     * 
206     * @param clicked flag to indicate if the button should be regarded as
207     *        having been clicked or not.
208     * @see #isClicked()
209     */
210    public void setClicked(boolean clicked) {
211        boolean old = isClicked();
212        hasBeenVisited = clicked;
213        setForeground(isClicked() ? getClickedColor() : getUnclickedColor());
214        firePropertyChange("clicked", old, isClicked());
215    }
216
217    /**
218     * Returns a boolean indicating if this link has already been visited.
219     * 
220     * @return <code>true</code> if hyper link has already been clicked.
221     * @see #setClicked(boolean)
222     */
223    public boolean isClicked() {
224        return hasBeenVisited;
225    }
226
227    /**
228     * Sets the overrulesActionOnClick property. It controls whether this
229     * button should overrule the Action's visited property on actionPerformed. <p>
230     * 
231     * The default value is <code>false</code>.
232     * 
233     * @param overrule if true, fireActionPerformed will set clicked to true
234     *   independent of action.
235     * 
236     * @see #getOverrulesActionOnClick()
237     * @see #setClicked(boolean)
238     */
239    public void setOverrulesActionOnClick(boolean overrule) {
240        boolean old = getOverrulesActionOnClick();
241        this.overrulesActionOnClick = overrule;
242        firePropertyChange("overrulesActionOnClick", old, getOverrulesActionOnClick());
243    }
244    
245    /**
246     * Returns a boolean indicating whether the clicked property should be set
247     * always on clicked.
248     * 
249     * @return overrulesActionOnClick false if his button clicked property
250     *         respects the Action's visited property. True if the clicked
251     *         should be updated on every actionPerformed.
252     * 
253     * @see #setOverrulesActionOnClick(boolean)
254     * @see #setClicked(boolean)
255     */
256    public boolean getOverrulesActionOnClick() {
257        return overrulesActionOnClick;
258    }
259
260    /**
261     * {@inheritDoc} <p>
262     * Overridden to respect the overrulesActionOnClick property.
263     */
264    @Override
265    protected void fireActionPerformed(ActionEvent event) {
266        super.fireActionPerformed(event);
267        if (isAutoSetClicked()) {
268            setClicked(true);
269        }
270    }
271
272    /**
273     * Returns a boolean indicating whether the clicked property should be set 
274     * after firing action events.
275     * Here: true if no action or overrulesAction property is true.
276     * @return true if fireActionEvent should force a clicked, false if not.
277     */
278    protected boolean isAutoSetClicked() {
279        return getAction() == null || getOverrulesActionOnClick();
280    }
281
282    /**
283     * Creates and returns a listener that will watch the changes of the
284     * provided <code>Action</code> and will update JXHyperlink's properties
285     * accordingly.
286     */
287    @Override
288    protected PropertyChangeListener createActionPropertyChangeListener(
289            final Action a) {
290        final PropertyChangeListener superListener = super
291                .createActionPropertyChangeListener(a);
292        // JW: need to do something better - only weak refs allowed!
293        // no way to hook into super
294        return new PropertyChangeListener() {
295
296            @Override
297            public void propertyChange(PropertyChangeEvent evt) {
298                if (AbstractHyperlinkAction.VISITED_KEY.equals(evt.getPropertyName())) {
299                    configureClickedPropertyFromAction(a);
300                } else {
301                    superListener.propertyChange(evt);
302                }
303
304            }
305
306        };
307    }
308
309    /**
310     * Read all the essential properties from the provided <code>Action</code>
311     * and apply it to the <code>JXHyperlink</code>
312     */
313    @Override
314    protected void configurePropertiesFromAction(Action a) {
315        super.configurePropertiesFromAction(a);
316        configureClickedPropertyFromAction(a);
317    }
318
319    private void configureClickedPropertyFromAction(Action a) {
320        boolean clicked = false;
321        if (a != null) {
322            clicked = Boolean.TRUE.equals(a.getValue(AbstractHyperlinkAction.VISITED_KEY));
323            
324        }
325        setClicked(clicked);
326    }
327
328    private void init() {
329        setForeground(isClicked() ? getClickedColor() : getUnclickedColor());
330    }
331
332    /**
333     * Returns a string that specifies the name of the L&F class
334     * that renders this component.
335     */
336    @Override
337    public String getUIClassID() {
338        return uiClassID;
339    }
340    
341    /**
342     * Notification from the <code>UIManager</code> that the L&F has changed.
343     * Replaces the current UI object with the latest version from the <code>UIManager</code>.
344     * 
345     * @see javax.swing.JComponent#updateUI
346     */
347    @Override
348    public void updateUI() {
349      setUI((ButtonUI)LookAndFeelAddons.getUI(this, ButtonUI.class));
350    }
351}