001    /**
002     * Copyright (c) 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.ext;
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.event.MouseEvent;
040    import java.awt.event.MouseWheelEvent;
041    import java.awt.event.MouseWheelListener;
042    import java.awt.geom.AffineTransform;
043    import java.awt.geom.NoninvertibleTransformException;
044    
045    import javax.swing.JComponent;
046    import javax.swing.SwingUtilities;
047    
048    import org.jdesktop.jxlayer.JXLayer;
049    import org.jdesktop.jxlayer.plaf.AbstractLayerUI;
050    import org.jdesktop.jxlayer.plaf.LayerUI;
051    
052    /**
053     * This class provides for {@link MouseEvent} re-dispatching. It may be used to
054     * set a tool tip on {@link JXLayer}'s glass pane and still have the child
055     * components receive {@link MouseEvent}s.
056     * <p>
057     * <b>Note:</b> A {@link MouseEventUI} instance cannot be shared and can be set
058     * to a single {@link JXLayer} instance only.
059     * <p/>
060     */
061    public class MouseEventUI<V extends JComponent> extends AbstractLayerUI<V> {
062    
063        private Component lastEnteredTarget, lastPressedTarget;
064    
065        private boolean dispatchingMode = false;
066    
067        private JXLayer<V> installedLayer;
068    
069        /**
070         * Overridden to allow for re-dispatching of mouse events to their intended
071         * (visual) recipients, rather than to the components according to their
072         * bounds.
073         */
074        @Override
075        public void eventDispatched(AWTEvent event, final JXLayer<V> layer) {
076    
077            if (event instanceof MouseEvent) {
078                if (!dispatchingMode) {
079                    dispatchingMode = true;
080                    try {
081                        redispatch((MouseEvent) event, layer);
082                    } finally {
083                        dispatchingMode = false;
084                    }
085                } else {
086                    Component component = ((MouseEvent) event).getComponent();
087                    layer.getGlassPane().setCursor(component.getCursor());
088                }
089            }
090        }
091    
092        /**
093         * Overridden to only get the following event types:
094         * {@link AWTEvent#MOUSE_EVENT_MASK},
095         * {@link AWTEvent#MOUSE_MOTION_EVENT_MASK} and
096         * {@link AWTEvent#MOUSE_WHEEL_EVENT_MASK}.
097         * 
098         */
099        @Override
100        public long getLayerEventMask() {
101            return AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK
102                    | AWTEvent.MOUSE_WHEEL_EVENT_MASK;
103        }
104    
105        /**
106         * Overridden to check if this {@link LayerUI} has not been installed
107         * already, and to set the argument {@code component} as the installed
108         * {@link JXLayer}.
109         * 
110         * @throws IllegalStateException
111         *             when this {@link LayerUI} has been installed already
112         * @see #getInstalledLayer()
113         */
114        @SuppressWarnings("unchecked")
115        @Override
116        public void installUI(JComponent component) throws IllegalStateException {
117            super.installUI(component);
118            if (installedLayer != null) {
119                throw new IllegalStateException(this.getClass().getName()
120                        + " cannot be shared between multiple layers");
121            }
122            installedLayer = (JXLayer<V>) component;
123        }
124    
125        /**
126         * Overridden to remove the installed {@link JXLayer}.
127         */
128        @Override
129        public void uninstallUI(JComponent c) {
130            installedLayer = null;
131            super.uninstallUI(c);
132        }
133    
134        private Point calculateTargetPoint(JXLayer<V> layer, MouseEvent mouseEvent) {
135            Point point = mouseEvent.getPoint();
136            Rectangle layerBounds = layer.getBounds();
137            Container parent = layer.getParent();
138            Rectangle parentRectangle = new Rectangle(-layerBounds.x,
139                    -layerBounds.y, parent.getWidth(), parent.getHeight());
140            SwingUtilities.convertPointToScreen(point, mouseEvent.getComponent());
141            SwingUtilities.convertPointFromScreen(point, layer);
142            if (parentRectangle.contains(point)) {
143                return transformPoint(layer, point);
144            } else {
145                return new Point(-1, -1);
146            }
147        }
148    
149        private MouseWheelEvent createMouseWheelEvent(
150                MouseWheelEvent mouseWheelEvent, Point point, Component target) {
151            return new MouseWheelEvent(target, //
152                    mouseWheelEvent.getID(), //
153                    mouseWheelEvent.getWhen(), //
154                    mouseWheelEvent.getModifiers(), //
155                    point.x, //
156                    point.y, //
157                    mouseWheelEvent.getClickCount(), //
158                    mouseWheelEvent.isPopupTrigger(), //
159                    mouseWheelEvent.getScrollType(), //
160                    mouseWheelEvent.getScrollAmount(), //
161                    mouseWheelEvent.getWheelRotation() //
162            );
163        }
164    
165        private void dispatchMouseEvent(MouseEvent mouseEvent) {
166            if (mouseEvent != null) {
167                Component target = mouseEvent.getComponent();
168                target.dispatchEvent(mouseEvent);
169            }
170        }
171    
172        private Component findWheelListenerComponent(Component target) {
173            if (target == null) {
174                return null;
175            } else if (target.getMouseWheelListeners().length == 0) {
176                return findWheelListenerComponent(target.getParent());
177            } else {
178                return target;
179            }
180        }
181    
182        private void generateEnterExitEvents(JXLayer<V> layer,
183                MouseEvent originalEvent, Component newTarget, Point realPoint) {
184            if (lastEnteredTarget != newTarget) {
185                dispatchMouseEvent(transformMouseEvent(layer, originalEvent,
186                        lastEnteredTarget, realPoint, MouseEvent.MOUSE_EXITED));
187                lastEnteredTarget = newTarget;
188                dispatchMouseEvent(transformMouseEvent(layer, originalEvent,
189                        lastEnteredTarget, realPoint, MouseEvent.MOUSE_ENTERED));
190            }
191        }
192    
193        private Component getListeningComponent(MouseEvent event,
194                Component component) {
195            switch (event.getID()) {
196            case (MouseEvent.MOUSE_CLICKED):
197            case (MouseEvent.MOUSE_ENTERED):
198            case (MouseEvent.MOUSE_EXITED):
199            case (MouseEvent.MOUSE_PRESSED):
200            case (MouseEvent.MOUSE_RELEASED):
201                return getMouseListeningComponent(component);
202            case (MouseEvent.MOUSE_DRAGGED):
203            case (MouseEvent.MOUSE_MOVED):
204                return getMouseMotionListeningComponent(component);
205            case (MouseEvent.MOUSE_WHEEL):
206                return getMouseWheelListeningComponent(component);
207            }
208            return null;
209        }
210    
211        private Component getMouseListeningComponent(Component component) {
212            if (component.getMouseListeners().length > 0) {
213                return component;
214            } else {
215                Container parent = component.getParent();
216                if (parent != null) {
217                    return getMouseListeningComponent(parent);
218                } else {
219                    return null;
220                }
221            }
222        }
223    
224        private Component getMouseMotionListeningComponent(Component component) {
225            /*
226             * Mouse motion events may result in MOUSE_ENTERED and MOUSE_EXITED.
227             * 
228             * Therefore, components with MouseListeners registered should be
229             * returned as well.
230             */
231            if (component.getMouseMotionListeners().length > 0
232                    || component.getMouseListeners().length > 0) {
233                return component;
234            } else {
235                Container parent = component.getParent();
236                if (parent != null) {
237                    return getMouseMotionListeningComponent(parent);
238                } else {
239                    return null;
240                }
241            }
242        }
243    
244        private Component getMouseWheelListeningComponent(Component component) {
245            if (component.getMouseWheelListeners().length > 0) {
246                return component;
247            } else {
248                Container parent = component.getParent();
249                if (parent != null) {
250                    return getMouseWheelListeningComponent(parent);
251                } else {
252                    return null;
253                }
254            }
255        }
256    
257        private Component getTarget(JXLayer<V> layer, Point targetPoint) {
258            Component view = layer.getView();
259            if (view == null) {
260                return null;
261            } else {
262                Point viewPoint = SwingUtilities.convertPoint(layer, targetPoint,
263                        view);
264                return SwingUtilities.getDeepestComponentAt(view, viewPoint.x,
265                        viewPoint.y);
266            }
267        }
268    
269        private void redispatch(MouseEvent originalEvent, final JXLayer<V> layer) {
270            if (layer.getView() != null) {
271                if (originalEvent.getComponent() != layer.getGlassPane()) {
272                    originalEvent.consume();
273                }
274                MouseEvent newEvent = null;
275    
276                Point realPoint = calculateTargetPoint(layer, originalEvent);
277                Component realTarget = getTarget(layer, realPoint);
278                if (realTarget != null) {
279                    realTarget = getListeningComponent(originalEvent, realTarget);
280                }
281    
282                switch (originalEvent.getID()) {
283                case MouseEvent.MOUSE_PRESSED:
284                    newEvent = transformMouseEvent(layer, originalEvent,
285                            realTarget, realPoint);
286                    if (newEvent != null) {
287                        lastPressedTarget = newEvent.getComponent();
288                    }
289                    break;
290                case MouseEvent.MOUSE_RELEASED:
291                    newEvent = transformMouseEvent(layer, originalEvent,
292                            lastPressedTarget, realPoint);
293                    lastPressedTarget = null;
294                    break;
295                case MouseEvent.MOUSE_ENTERED:
296                    generateEnterExitEvents(layer, originalEvent, realTarget,
297                            realPoint);
298                    break;
299                case MouseEvent.MOUSE_EXITED:
300                    generateEnterExitEvents(layer, originalEvent, realTarget,
301                            realPoint);
302                    break;
303                case MouseEvent.MOUSE_MOVED:
304                    newEvent = transformMouseEvent(layer, originalEvent,
305                            realTarget, realPoint);
306                    generateEnterExitEvents(layer, originalEvent, realTarget,
307                            realPoint);
308                    break;
309                case MouseEvent.MOUSE_DRAGGED:
310                    newEvent = transformMouseEvent(layer, originalEvent,
311                            lastPressedTarget, realPoint);
312                    generateEnterExitEvents(layer, originalEvent, realTarget,
313                            realPoint);
314                    break;
315                case MouseEvent.MOUSE_CLICKED:
316                    newEvent = transformMouseEvent(layer, originalEvent,
317                            realTarget, realPoint);
318                    break;
319                case (MouseEvent.MOUSE_WHEEL):
320                    redispatchMouseWheelEvent((MouseWheelEvent) originalEvent,
321                            realTarget, layer);
322                    break;
323                }
324                dispatchMouseEvent(newEvent);
325            }
326        }
327    
328        private void redispatchMouseWheelEvent(MouseWheelEvent mouseWheelEvent,
329                Component target, JXLayer<V> layer) {
330            MouseWheelEvent newEvent = this.transformMouseWheelEvent(
331                    mouseWheelEvent, target, layer);
332            processMouseWheelEvent(newEvent, layer);
333        }
334    
335        private MouseEvent transformMouseEvent(JXLayer<V> layer,
336                MouseEvent mouseEvent, Component target, Point realPoint) {
337            return transformMouseEvent(layer, mouseEvent, target, realPoint,
338                    mouseEvent.getID());
339        }
340    
341        private MouseEvent transformMouseEvent(JXLayer<V> layer,
342                MouseEvent mouseEvent, Component target, Point targetPoint, int id) {
343            if (target == null) {
344                return null;
345            } else {
346                Point newPoint = new Point(targetPoint);
347                SwingUtilities.convertPointToScreen(newPoint, layer);
348                SwingUtilities.convertPointFromScreen(newPoint, target);
349                return new MouseEvent(target, //
350                        id, //
351                        mouseEvent.getWhen(), //
352                        mouseEvent.getModifiers(), //
353                        newPoint.x, //
354                        newPoint.y, //
355                        mouseEvent.getClickCount(), //
356                        mouseEvent.isPopupTrigger(), //
357                        mouseEvent.getButton());
358            }
359        }
360    
361        private MouseWheelEvent transformMouseWheelEvent(
362                MouseWheelEvent mouseWheelEvent, Component target, JXLayer<V> layer) {
363            if (target == null) {
364                target = layer;
365            }
366            Point point = SwingUtilities.convertPoint(mouseWheelEvent
367                    .getComponent(), mouseWheelEvent.getPoint(), target);
368            MouseWheelEvent newEvent = createMouseWheelEvent(mouseWheelEvent,
369                    point, target);
370            return newEvent;
371        }
372    
373        private Point transformPoint(JXLayer<V> layer, Point point) {
374            AffineTransform transform = this.getTransform(layer);
375            if (transform != null) {
376                try {
377                    transform.inverseTransform(point, point);
378                } catch (NoninvertibleTransformException e) {
379                    e.printStackTrace();
380                }
381            }
382            return point;
383        }
384    
385        protected JXLayer<V> getInstalledLayer() {
386            return installedLayer;
387        }
388    
389        /**
390         * Re-dispatches the event to the first component in the hierarchy that has
391         * a {@link MouseWheelListener} registered.
392         */
393        @Override
394        protected void processMouseWheelEvent(MouseWheelEvent event,
395                JXLayer<V> jxlayer) {
396            /*
397             * Only process an event if it is not already consumed. This may be the
398             * case if this LayerUI is contained in a wrapped hierarchy.
399             */
400            if (!event.isConsumed()) {
401                /*
402                 * Since we will create a new event, the argument event must be
403                 * consumed.
404                 */
405                event.consume();
406                /*
407                 * Find a target up in the hierarchy that has
408                 * MouseWheelEventListeners registered.
409                 */
410                Component target = event.getComponent();
411                Component newTarget = findWheelListenerComponent(target);
412                if (newTarget == null) {
413                    newTarget = jxlayer.getParent();
414                }
415                /*
416                 * Convert the location relative to the new target
417                 */
418                Point point = SwingUtilities.convertPoint(event.getComponent(),
419                        event.getPoint(), newTarget);
420                /*
421                 * Create a new event and dispatch it.
422                 */
423                newTarget.dispatchEvent(createMouseWheelEvent(event, point,
424                        newTarget));
425            }
426        }
427    
428    }