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