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