001/*
002 * $Id: RolloverController.java 3967 2011-03-17 19:18:47Z kschaefe $
003 *
004 * Copyright 2006 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.rollover;
022
023import java.awt.Point;
024import java.awt.event.ActionEvent;
025import java.beans.PropertyChangeEvent;
026import java.beans.PropertyChangeListener;
027import java.util.logging.Logger;
028
029import javax.swing.Action;
030import javax.swing.JComponent;
031import javax.swing.KeyStroke;
032
033import org.jdesktop.swingx.plaf.UIAction;
034
035/**
036 * Controller for "live" behaviour of XXRenderers.
037 * 
038 * Once installed on a component, it updates renderer's rollover 
039 * state based on the component's rollover properties. Rollover
040 * client properties are Points with cell coordinates 
041 * in the view coordinate
042 * system as appropriate for the concrete component 
043 * (Point.x == column, Point.y == row).
044 * 
045 * Repaints effected component regions. Updates
046 * link cursor. Installs a click-action bound to space-released in the target's
047 * actionMap/inputMap.
048 * 
049 * 
050 * @author Jeanette Winzenburg, Berlin
051 */
052public abstract class RolloverController<T extends JComponent> implements
053        PropertyChangeListener {
054    @SuppressWarnings("unused")
055    private static final Logger LOG = Logger.getLogger(RolloverController.class
056            .getName());
057    /**
058     * the key of the rollover click action which is installed in the 
059     * component's actionMap.
060     */
061    public static final String EXECUTE_BUTTON_ACTIONCOMMAND = "executeButtonAction";
062
063    protected T component;
064
065    @Override
066    public void propertyChange(PropertyChangeEvent evt) {
067        // JW: should not happen ... being paranoid. 
068        if ((component == null) || (component != evt.getSource()))
069            return;
070        if (RolloverProducer.ROLLOVER_KEY.equals(evt.getPropertyName())) {
071            rollover((Point) evt.getOldValue(), (Point) evt.getNewValue());
072        } else if (RolloverProducer.CLICKED_KEY.equals(evt.getPropertyName())) {
073            click((Point) evt.getNewValue());
074        }
075    }
076
077    /**
078     * Install this as controller for the given component.
079     * 
080     * @param table the component which has renderers to control.
081     */
082    public void install(T table) {
083        release();
084        this.component = table;
085        table.addPropertyChangeListener(RolloverProducer.CLICKED_KEY, this);
086        table.addPropertyChangeListener(RolloverProducer.ROLLOVER_KEY, this);
087        registerExecuteButtonAction();
088    }
089
090    /**
091     * Uninstall this as controller from the component, if any.
092     *
093     */
094    public void release() {
095        if (component == null)
096            return;
097        component.removePropertyChangeListener(RolloverProducer.CLICKED_KEY, this);
098        component.removePropertyChangeListener(RolloverProducer.ROLLOVER_KEY, this);
099        unregisterExecuteButtonAction();
100        component = null;
101    }
102
103    /**
104     * called on change of client property Rollover_Key.
105     * 
106     * @param oldLocation the old value of the rollover location.
107     * @param newLocation the new value of the rollover location.
108     */
109    protected abstract void rollover(Point oldLocation, Point newLocation);
110
111    /**
112     * called on change of client property Clicked_key.
113     * @param location the new value of the clicked location.
114     */
115    protected void click(Point location) {
116        if (!isClickable(location))
117            return;
118        RolloverRenderer rollover = getRolloverRenderer(location, true);
119        if (rollover != null) {
120            rollover.doClick();
121            component.repaint();
122        }
123    }
124
125    /**
126     * Returns the rolloverRenderer at the given location. <p>
127     * 
128     * The result
129     * may be null if there is none or if rollover is not enabled.
130     * 
131     * If the prepare flag is true, the renderer will be prepared 
132     * with value and state as appropriate for the given location.
133     * 
134     * Note: PRE - the location must be valid in cell coordinate space.
135     * 
136     * @param location a valid location in cell coordinates, p.x == column, p.y == row.
137     * @param prepare 
138     * @return <code>RolloverRenderer</code> at the given location
139     */
140    protected abstract RolloverRenderer getRolloverRenderer(Point location,
141            boolean prepare);
142
143    /**
144     * Returns a boolean indicating whether or not the cell at the given 
145     * location is clickable. <p>
146     * 
147     * This implementation returns true if the target is enabled and the
148     * cell has a rollover renderer.
149     * 
150     * @param location in cell coordinates, p.x == column, p.y == row.
151     * @return true if the cell at the given location is clickable
152     * 
153     * @see #hasRollover(Point)
154     */
155    protected boolean isClickable(Point location) {
156        return component.isEnabled() && hasRollover(location);
157    }
158
159    /**
160     * Returns a boolean indicating whether the or not the cell at the 
161     * given has a rollover renderer. Always returns false if the location
162     * is not valid.
163     * 
164     * @param location in cell coordinates, p.x == column, p.y == row.
165     * @return true if the location is valid and has rollover effects, false
166     *   otherwise.
167     *    
168     */
169    protected boolean hasRollover(Point location) {
170        if (location == null || location.x < 0 || location.y < 0)
171            return false;
172        return getRolloverRenderer(location, false) != null;
173    }
174    
175    /**
176     * The coordinates of the focused cell in view coordinates. 
177     * 
178     * This method is called if the click action is invoked by a keyStroke.
179     * The returned cell coordinates should be related to
180     * what is typically interpreted as "focused" in the context of the
181     * component.
182     * 
183     * p.x == focused column, p.y == focused row. 
184     * A null return value or any coordinate value of < 0  
185     * is interpreted as "outside".
186     * 
187     * @return the location of the focused cell.
188     */
189    protected abstract Point getFocusedCell();
190
191    /**
192     * uninstalls and deregisters the click action from the component's 
193     * actionMap/inputMap. 
194     *
195     */
196    protected void unregisterExecuteButtonAction() {
197        component.getActionMap().put(EXECUTE_BUTTON_ACTIONCOMMAND, null);
198        KeyStroke space = KeyStroke.getKeyStroke("released SPACE");
199        component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
200                space, null);
201    }
202
203    /**
204     * installs and registers the click action in the component's 
205     * actionMap/inputMap. 
206     *
207     */
208    protected void registerExecuteButtonAction() {
209        component.getActionMap().put(EXECUTE_BUTTON_ACTIONCOMMAND,
210                createExecuteButtonAction());
211        KeyStroke space = KeyStroke.getKeyStroke("released SPACE");
212        component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(
213                space, EXECUTE_BUTTON_ACTIONCOMMAND);
214
215    }
216
217    /**
218     * creates and returns the click action to install in the 
219     * component's actionMap.
220     *
221     */
222    protected Action createExecuteButtonAction() {
223        return new UIAction(null) {
224            @Override
225            public void actionPerformed(ActionEvent e) {
226                click(getFocusedCell());
227            }
228
229            @Override
230            public boolean isEnabled(Object sender) {
231                if (component == null || !component.isEnabled() || !component.hasFocus())
232                    return false;
233                return isClickable(getFocusedCell());
234            }
235        };
236    }
237
238}