001/*
002 * $Id: RolloverProducer.java 3982 2011-03-30 12:27:31Z kleopatra $
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.rollover;
022
023import java.awt.Point;
024import java.awt.event.ComponentEvent;
025import java.awt.event.ComponentListener;
026import java.awt.event.MouseEvent;
027import java.awt.event.MouseListener;
028import java.awt.event.MouseMotionListener;
029import java.util.logging.Logger;
030
031import javax.swing.JComponent;
032
033/**
034 * Mouse/Motion/Listener which maps mouse coordinates to client coordinates
035 * and stores these as client properties in the target JComponent. The exact
036 * mapping process is left to subclasses. Typically, they will map to "cell"
037 * coordinates. <p>
038 * 
039 * Note: this class assumes that the target component is of type JComponent.<p>
040 * Note: this implementation is stateful, it can't be shared across different 
041 * instances of a target component.<p>
042 * 
043 * 
044 * @author Jeanette Winzenburg
045 */
046public abstract class RolloverProducer implements MouseListener, MouseMotionListener, 
047   ComponentListener {
048
049    @SuppressWarnings("unused")
050    private static final Logger LOG = Logger.getLogger(RolloverProducer.class
051            .getName());
052    
053    /** 
054     * Key for client property mapped from mouse-triggered action.
055     * Note that the actual mouse-event which results in setting the property
056     * depends on the implementation of the concrete RolloverProducer. 
057     */
058    public static final String CLICKED_KEY = "swingx.clicked";
059
060    /** Key for client property mapped from rollover events */
061    public static final String ROLLOVER_KEY = "swingx.rollover";
062
063    //        public static final String PRESSED_KEY = "swingx.pressed";
064
065    private boolean isDragging;
066    
067    /**
068     * Installs all listeners, as required. 
069     * 
070     * @param component target to install required listeners on, must
071     *   not be null.
072     */
073    public void install(JComponent component) {
074        component.addMouseListener(this);
075        component.addMouseMotionListener(this);
076        component.addComponentListener(this);
077    }
078    
079    /**
080     * Removes all listeners.
081     * 
082     * @param component target component to uninstall required listeners from, 
083     *   must not be null
084     */
085    public void release(JComponent component) {
086        component.removeMouseListener(this);
087        component.removeMouseMotionListener(this);
088        component.removeComponentListener(this);
089    }
090    
091    //----------------- mouseListener
092
093    /**
094     * Implemented to map to Rollover properties as needed. This implemenation calls
095     * updateRollover with both ROLLOVER_KEY and CLICKED_KEY properties. 
096     */
097    @Override
098    public void mouseReleased(MouseEvent e) {
099        Point oldCell = new Point(rollover); 
100        // JW: fix for #456-swingx - rollover not updated after end of dragging
101        updateRollover(e, ROLLOVER_KEY, false);
102        // Fix Issue 1387-swingx - no click on release-after-drag
103        if (isClick(e, oldCell, isDragging)) {
104            updateRollover(e, CLICKED_KEY, true);
105        }
106        isDragging = false;
107    }
108
109    /**
110     * Returns a boolean indicating whether or not the given mouse event should
111     * be interpreted as a click. This method is called from mouseReleased
112     * after the cell coordiates were updated. While the ID of mouse event
113     * is not formally enforced, it is assumed to be a MOUSE_RELEASED. Calling
114     * for other types might or might not work as expected. <p>
115     * 
116     * This implementation returns true if the current rollover point is the same
117     * cell as the given oldRollover, that is ending a drag inside the same cell
118     * triggers the action while ending a drag somewhere does not. <p>
119     * 
120     * PENDING JW: open to more complex logic in case it clashes with existing code,
121     * see Issue #1387. 
122     * 
123     * @param e the mouseEvent which triggered calling this, assumed to be 
124     *    a mouseReleased, must not be null
125     * @param oldRollover the cell before the mouseEvent was mapped, must not be null
126     * @param wasDragging true if the release happened
127     * @return a boolean indicating whether or not the given mouseEvent should
128     *   be interpreted as a click.
129     */
130    protected boolean isClick(MouseEvent e, Point oldRollover, boolean wasDragging) {
131        return oldRollover.equals(rollover);
132    }
133
134    /**
135     * Implemented to map to client property rollover and fire only if client
136     * coordinate changed.
137     */
138    @Override
139    public void mouseEntered(MouseEvent e) {
140//        LOG.info("" + e);
141        isDragging = false;
142        updateRollover(e, ROLLOVER_KEY, false);
143    }
144
145    /**
146     * Implemented to remove client properties rollover and clicked. if the
147     * source is a JComponent. Does nothing otherwise.
148     */
149    @Override
150    public void mouseExited(MouseEvent e) {
151        isDragging = false;
152//        screenLocation = null;
153//        LOG.info("" + e);
154//        if (((JComponent) e.getComponent()).getMousePosition(true) != null)  {
155//            updateRollover(e, ROLLOVER_KEY, false);
156//        } else {
157//        }
158        ((JComponent) e.getSource()).putClientProperty(ROLLOVER_KEY, null);
159        ((JComponent) e.getSource()).putClientProperty(CLICKED_KEY, null);
160            
161        }
162
163    /**
164     * Implemented to do nothing.
165     */
166    @Override
167    public void mouseClicked(MouseEvent e) {
168    }
169
170    /**
171     * Implemented to do nothing.
172     */
173    @Override
174    public void mousePressed(MouseEvent e) {
175    }
176
177    // ---------------- MouseMotionListener
178    /**
179     * Implemented to set a dragging flag to true.
180     */
181    @Override
182    public void mouseDragged(MouseEvent e) {
183        isDragging = true;
184    }
185
186    /**
187     * Implemented to map to client property rollover and fire only if client
188     * coordinate changed.
189     */
190    @Override
191    public void mouseMoved(MouseEvent e) {
192        updateRollover(e, ROLLOVER_KEY, false);
193    }
194
195    //---------------- ComponentListener
196    
197    
198    @Override
199    public void componentShown(ComponentEvent e) {
200    }
201    
202    @Override
203    public void componentResized(ComponentEvent e) {
204        updateRollover(e);
205    }
206    
207    @Override
208    public void componentMoved(ComponentEvent e) {
209        updateRollover(e);
210    }
211
212    /**
213     * @param e
214     */
215    private void updateRollover(ComponentEvent e) {
216        Point componentLocation = e.getComponent().getMousePosition();
217        if (componentLocation == null) {
218            componentLocation = new Point(-1, -1);
219        }
220//        LOG.info("" + componentLocation + " / " + e);
221        updateRolloverPoint((JComponent) e.getComponent(), componentLocation);
222        updateClientProperty((JComponent) e.getComponent(), ROLLOVER_KEY, true);
223    }
224    
225    @Override
226    public void componentHidden(ComponentEvent e) {
227    }
228
229    //---------------- mapping methods
230    
231    /**
232     * Controls the mapping of the given mouse event to a client property. This
233     * implementation first calls updateRolloverPoint to convert the mouse coordinates.
234     * Then calls updateClientProperty to actually set the client property in the
235     * 
236     * @param e the MouseEvent to map to client coordinates
237     * @param property the client property to map to
238     * @param fireAlways a flag indicating whether a client event should be fired if unchanged.
239     * 
240     * @see #updateRolloverPoint(JComponent, Point)
241     * @see #updateClientProperty(JComponent, String, boolean)
242     */
243    protected void updateRollover(MouseEvent e, String property,
244            boolean fireAlways) {
245        updateRolloverPoint((JComponent) e.getComponent(), e.getPoint());
246        updateClientProperty((JComponent) e.getComponent(), property, fireAlways);
247    }
248
249    /** Current mouse location in client coordinates. */
250    protected Point rollover = new Point(-1, -1);
251
252    /**
253     * Sets the given client property to the value of current mouse location in 
254     * client coordinates. If fireAlways, the property is force to fire a change.
255     *  
256     * @param component the target component
257     * @param property the client property to set
258     * @param fireAlways a flag indicating whether a client property 
259     *  should be forced to fire an event.
260     */
261    protected void updateClientProperty(JComponent component, String property,
262            boolean fireAlways) {
263        if (fireAlways) {
264            // fix Issue #864-swingx: force propertyChangeEvent
265            component.putClientProperty(property, null);
266            component.putClientProperty(property, new Point(rollover));
267        } else {
268            Point p = (Point) component.getClientProperty(property);
269            if (p == null || (rollover.x != p.x) || (rollover.y != p.y)) {
270                component.putClientProperty(property, new Point(rollover));
271            }
272        }
273    }
274
275    /**
276     * Subclasses must implement to map the given mouse coordinates into
277     * appropriate client coordinates. The result must be stored in the 
278     * rollover field. 
279     * 
280     * @param component the target component which received a mouse event
281     * @param mousePoint the mouse position of the event, coordinates are 
282     *    component pixels
283     */
284    protected abstract void updateRolloverPoint(JComponent component, Point mousePoint);
285
286}