001    /**
002     * Copyright (c) 2008-2009, Piet Blok
003     * All rights reserved.
004     *
005     * Redistribution and use in source and binary forms, with or without
006     * modification, are permitted provided that the following conditions
007     * are met:
008     *
009     *   * Redistributions of source code must retain the above copyright
010     *     notice, this list of conditions and the following disclaimer.
011     *   * Redistributions in binary form must reproduce the above
012     *     copyright notice, this list of conditions and the following
013     *     disclaimer in the documentation and/or other materials provided
014     *     with the distribution.
015     *   * Neither the name of the copyright holder nor the names of the
016     *     contributors may be used to endorse or promote products derived
017     *     from this software without specific prior written permission.
018     *
019     * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
020     * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
021     * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
022     * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
023     * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
024     * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
025     * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
026     * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
027     * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
028     * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
029     * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
030     */
031    
032    package org.pbjar.jxlayer.plaf.misc;
033    
034    import java.awt.AWTEvent;
035    import java.awt.Component;
036    import java.awt.Container;
037    import java.awt.Point;
038    import java.awt.Rectangle;
039    import java.awt.Toolkit;
040    import java.awt.event.AWTEventListener;
041    import java.awt.event.MouseEvent;
042    import java.awt.event.MouseWheelEvent;
043    
044    import javax.swing.JComponent;
045    import javax.swing.SwingUtilities;
046    
047    import org.jdesktop.jxlayer.JXLayer;
048    
049    /**
050     * Manages mouse events for {@link JXLayer}. Mouse events targeted at the glass
051     * pane are dispatched to the glass pane itself and to the children in the
052     * component hierarchy. Other mouse events are dispatched only to the children.
053     * <p>
054     * Sub classes may apply transformations in order to select a different child
055     * component than the component associated with the event..
056     * </p>
057     * 
058     * <p>
059     * The {@code AbstractTransparentMouseUI} class is <b>not</b> designed for
060     * shared use, but shared use <em>may</em> work. I just didn't test it.
061     * </p>
062     * 
063     * @author Piet Blok
064     * 
065     * @param <V>
066     *            JXLayer's view
067     * @param <S>
068     *            A state object
069     */
070    public abstract class AbstractTransparentMouseUI<V extends JComponent, S> extends
071            GeneralLayerUI<V, S> {
072    
073        private Component lastEnteredTarget, lastPressedTarget;
074    
075        private boolean dispatchingMode = false;
076    
077        /**
078         * Construct a new {@code UI} with {@link AWTEventListener} disabled.
079         */
080        public AbstractTransparentMouseUI() {
081            this(false);
082        }
083    
084        /**
085         * Construct a new {@code UI}.
086         * 
087         * @param enableAWTEventListener
088         *            if {@code true}, {@link AWTEventListener} will be enabled.
089         *            Specifying {@code true} solves a problem with non editable
090         *            {@code JTextComponent}s.
091         * @throws SecurityException
092         *             when enableAWTEventListener is {@code true} and if a security
093         *             manager exists and its {@code checkPermission} method doesn't
094         *             allow the operation.
095         * @see Toolkit#addAWTEventListener(AWTEventListener, long)
096         */
097        public AbstractTransparentMouseUI(boolean enableAWTEventListener) {
098            super(enableAWTEventListener);
099        }
100    
101        /**
102         * Overridden to allow for re dispatching of mouse events to their intended
103         * (visual) recipients, rather than to the components according to their
104         * bounds.
105         */
106        @Override
107        public void eventDispatched(AWTEvent event, final JXLayer<V> layer) {
108    
109            if (event instanceof MouseEvent) {
110                if (!dispatchingMode) {
111                    dispatchingMode = true;
112                    try {
113                        redispatch((MouseEvent) event, layer);
114                    } finally {
115                        dispatchingMode = false;
116                    }
117                } else {
118                    Component component = ((MouseEvent) event).getComponent();
119                    layer.getGlassPane().setCursor(component.getCursor());
120                }
121            }
122        }
123    
124        /**
125         * Overridden to only get the following event types:
126         * {@link AWTEvent#MOUSE_EVENT_MASK},
127         * {@link AWTEvent#MOUSE_MOTION_EVENT_MASK} and
128         * {@link AWTEvent#MOUSE_WHEEL_EVENT_MASK}.
129         * 
130         */
131        @Override
132        public long getLayerEventMask() {
133            return AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK
134                    | AWTEvent.MOUSE_WHEEL_EVENT_MASK;
135        }
136    
137        private Point calculateTargetPoint(JXLayer<V> layer, MouseEvent mouseEvent) {
138            Point point = mouseEvent.getPoint();
139            V view = layer.getView();
140            Rectangle layerBounds = layer.getBounds();
141            Container parent = layer.getParent();
142            Rectangle parentRectangle = new Rectangle(-layerBounds.x,
143                    -layerBounds.y, parent.getWidth(), parent.getHeight());
144            SwingUtilities.convertPointToScreen(point, mouseEvent.getComponent());
145            SwingUtilities.convertPointFromScreen(point, view);
146            if (parentRectangle.contains(point)) {
147                return transformPoint(layer, point);
148            } else {
149                // Return a point outside the layer.
150                return new Point(-1, -1);
151            }
152        }
153    
154        private void dispatchMouseEvent(MouseEvent mouseEvent) {
155            if (mouseEvent != null) {
156                Component target = mouseEvent.getComponent();
157                target.dispatchEvent(mouseEvent);
158            }
159        }
160    
161        private void generateEnterExitEvents(JXLayer<V> layer,
162                MouseEvent originalEvent, Component newTarget, Point realPoint) {
163            if (lastEnteredTarget != newTarget) {
164                dispatchMouseEvent(transformMouseEvent(layer, originalEvent,
165                        lastEnteredTarget, realPoint, MouseEvent.MOUSE_EXITED));
166                lastEnteredTarget = newTarget;
167                dispatchMouseEvent(transformMouseEvent(layer, originalEvent,
168                        lastEnteredTarget, realPoint, MouseEvent.MOUSE_ENTERED));
169            }
170        }
171    
172        /**
173         * Find the first component up in the hierarchy that may be listening for a
174         * specific event.
175         * 
176         * @param event
177         *            the event
178         * @param component
179         *            the first component to investigate
180         * @return A component that is listening for this type of event, or {@code
181         *         null} if such a component can not be found.
182         */
183        private Component getListeningComponent(MouseEvent event,
184                Component component) {
185            switch (event.getID()) {
186            case (MouseEvent.MOUSE_CLICKED):
187            case (MouseEvent.MOUSE_ENTERED):
188            case (MouseEvent.MOUSE_EXITED):
189            case (MouseEvent.MOUSE_PRESSED):
190            case (MouseEvent.MOUSE_RELEASED):
191                return getMouseListeningComponent(component);
192            case (MouseEvent.MOUSE_DRAGGED):
193            case (MouseEvent.MOUSE_MOVED):
194                return getMouseMotionListeningComponent(component);
195            case (MouseEvent.MOUSE_WHEEL):
196                return getMouseWheelListeningComponent(component);
197            }
198            return null;
199        }
200    
201        private Component getMouseListeningComponent(Component component) {
202            if (component.getMouseListeners().length > 0) {
203                return component;
204            } else {
205                Container parent = component.getParent();
206                if (parent != null) {
207                    return getMouseListeningComponent(parent);
208                } else {
209                    return null;
210                }
211            }
212        }
213    
214        private Component getMouseMotionListeningComponent(Component component) {
215            /*
216             * Mouse motion events may result in MOUSE_ENTERED and MOUSE_EXITED.
217             * 
218             * Therefore, components with MouseListeners registered should be
219             * returned as well.
220             */
221            if (component.getMouseMotionListeners().length > 0
222                    || component.getMouseListeners().length > 0) {
223                return component;
224            } else {
225                Container parent = component.getParent();
226                if (parent != null) {
227                    return getMouseMotionListeningComponent(parent);
228                } else {
229                    return null;
230                }
231            }
232        }
233    
234        private Component getMouseWheelListeningComponent(Component component) {
235            if (component.getMouseWheelListeners().length > 0) {
236                return component;
237            } else {
238                Container parent = component.getParent();
239                if (parent != null) {
240                    return getMouseWheelListeningComponent(parent);
241                } else {
242                    return null;
243                }
244            }
245        }
246    
247        private Component getTarget(JXLayer<V> layer, Point targetPoint) {
248            Component userTopLevel = getTopLevelUserComponent(layer);
249            if (userTopLevel == null) {
250                return null;
251            } else {
252                Point viewPoint = SwingUtilities.convertPoint(layer.getView(),
253                        targetPoint, userTopLevel);
254                return SwingUtilities.getDeepestComponentAt(userTopLevel,
255                        viewPoint.x, viewPoint.y);
256            }
257        }
258    
259        private void redispatch(MouseEvent originalEvent, final JXLayer<V> layer) {
260    
261            V view = layer.getView();
262            if (view != null) {
263                if (originalEvent.getComponent() != layer.getGlassPane()) {
264                    originalEvent.consume();
265                }
266                MouseEvent newEvent = null;
267    
268                Point realPoint = calculateTargetPoint(layer, originalEvent);
269                Component realTarget = getTarget(layer, realPoint);
270                if (realTarget != null) {
271                    realTarget = getListeningComponent(originalEvent, realTarget);
272                }
273    
274                switch (originalEvent.getID()) {
275                case MouseEvent.MOUSE_RELEASED:
276                    newEvent = transformMouseEvent(layer, originalEvent,
277                            lastPressedTarget, realPoint);
278                    lastPressedTarget = null;
279                    break;
280                case MouseEvent.MOUSE_ENTERED:
281                    // is ignored, see MOUSE_MOVED / MOUSE_DRAGGED
282                    generateEnterExitEvents(layer, originalEvent, realTarget,
283                            realPoint);
284                    break;
285                case MouseEvent.MOUSE_EXITED:
286                    // is ignored, see MOUSE_MOVED / MOUSE_DRAGGED
287                    generateEnterExitEvents(layer, originalEvent, realTarget,
288                            realPoint);
289                    break;
290                case MouseEvent.MOUSE_CLICKED:
291                    newEvent = transformMouseEvent(layer, originalEvent,
292                            realTarget, realPoint);
293                    break;
294                case MouseEvent.MOUSE_PRESSED:
295                    newEvent = transformMouseEvent(layer, originalEvent,
296                            realTarget, realPoint);
297                    if (newEvent != null) {
298                        lastPressedTarget = newEvent.getComponent();
299                    }
300                    break;
301                case MouseEvent.MOUSE_MOVED:
302                    newEvent = transformMouseEvent(layer, originalEvent,
303                            realTarget, realPoint);
304                    generateEnterExitEvents(layer, originalEvent, realTarget,
305                            realPoint);
306                    break;
307                case MouseEvent.MOUSE_DRAGGED:
308                    newEvent = transformMouseEvent(layer, originalEvent,
309                            lastPressedTarget, realPoint);
310                    generateEnterExitEvents(layer, originalEvent, realTarget,
311                            realPoint);
312                    break;
313                case (MouseEvent.MOUSE_WHEEL):
314                    redispatchMouseWheelEvent((MouseWheelEvent) originalEvent,
315                            realTarget, layer);
316                    break;
317                }
318                dispatchMouseEvent(newEvent);
319            }
320        }
321    
322        /**
323         * Re-dispatch a MouseWheelEvent. Two steps are performed:
324         * <ol>
325         * <li>Invoke
326         * {@link #transformMouseWheelEvent(MouseWheelEvent, Component, JXLayer)}
327         * creates a new event.</li>
328         * <li>Invoke {@link #processMouseWheelEvent(MouseWheelEvent, JXLayer)} with
329         * the new event.</li>
330         * </ol>
331         * 
332         * @param mouseWheelEvent
333         *            the event
334         * @param target
335         *            the target component
336         * @param layer
337         *            the layer
338         */
339        private void redispatchMouseWheelEvent(MouseWheelEvent mouseWheelEvent,
340                Component target, JXLayer<V> layer) {
341            MouseWheelEvent newEvent = this.transformMouseWheelEvent(
342                    mouseWheelEvent, target, layer);
343            processMouseWheelEvent(newEvent, layer);
344        }
345    
346        private MouseEvent transformMouseEvent(JXLayer<V> layer,
347                MouseEvent mouseEvent, Component target, Point realPoint) {
348            return transformMouseEvent(layer, mouseEvent, target, realPoint,
349                    mouseEvent.getID());
350        }
351    
352        private MouseEvent transformMouseEvent(JXLayer<V> layer,
353                MouseEvent mouseEvent, Component target, Point targetPoint, int id) {
354            if (target == null) {
355                return null;
356            } else {
357                Point newPoint = new Point(targetPoint);
358                SwingUtilities.convertPointToScreen(newPoint, layer.getView());
359                SwingUtilities.convertPointFromScreen(newPoint, target);
360                return new MouseEvent(target, //
361                        id, //
362                        mouseEvent.getWhen(), //
363                        mouseEvent.getModifiers(), //
364                        newPoint.x, //
365                        newPoint.y, //
366                        mouseEvent.getClickCount(), //
367                        mouseEvent.isPopupTrigger(), //
368                        mouseEvent.getButton());
369            }
370        }
371    
372        /**
373         * Transforms a {@code MouseWheelEvent} to a new event with the right
374         * coordinates and the target source component.
375         * 
376         * @param mouseWheelEvent
377         *            the event
378         * @param target
379         *            the target component
380         * @param layer
381         *            the layer
382         * 
383         * @return a new event
384         */
385        private MouseWheelEvent transformMouseWheelEvent(
386                MouseWheelEvent mouseWheelEvent, Component target, JXLayer<V> layer) {
387            if (target == null) {
388                target = layer;
389            }
390            Point point = SwingUtilities.convertPoint(mouseWheelEvent
391                    .getComponent(), mouseWheelEvent.getPoint(), target);
392            MouseWheelEvent newEvent = new MouseWheelEvent(target, //
393                    mouseWheelEvent.getID(), //
394                    mouseWheelEvent.getWhen(), //
395                    mouseWheelEvent.getModifiers(), //
396                    point.x, //
397                    point.y, //
398                    mouseWheelEvent.getClickCount(), //
399                    mouseWheelEvent.isPopupTrigger(), //
400                    mouseWheelEvent.getScrollType(), //
401                    mouseWheelEvent.getScrollAmount(), //
402                    mouseWheelEvent.getWheelRotation() //
403            );
404            return newEvent;
405        }
406    
407        /**
408         * Return some component in this hierarchy that serves as the start for a
409         * search for deeper components.
410         * 
411         * @param layer
412         *            The layer
413         * @return a component
414         */
415        protected abstract Component getTopLevelUserComponent(JXLayer<V> layer);
416    
417        /**
418         * When transformations are used, transform a point in device space to a
419         * point in component space.
420         * 
421         * @param layer
422         *            The layer
423         * @param point
424         *            A point
425         * @return a transformed point
426         */
427        protected abstract Point transformPoint(JXLayer<V> layer, Point point);
428    
429    }