001    /*
002     * Copyright 2002-2004 the original author or authors.
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     *
008     * http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.util;
017    
018    import org.springframework.richclient.components.MessagableTabbedPane;
019    import org.springframework.richclient.components.MayHaveMessagableTab;
020    
021    import java.awt.Component;
022    import java.awt.Container;
023    import java.awt.Dimension;
024    import java.awt.LayoutManager;
025    import java.awt.Rectangle;
026    import java.awt.Point;
027    import java.awt.event.ComponentEvent;
028    import java.awt.event.ComponentListener;
029    import java.awt.event.HierarchyBoundsListener;
030    import java.awt.event.HierarchyEvent;
031    import java.awt.event.HierarchyListener;
032    import java.beans.PropertyChangeEvent;
033    import java.beans.PropertyChangeListener;
034    
035    import javax.swing.JComponent;
036    import javax.swing.JLayeredPane;
037    import javax.swing.SwingConstants;
038    import javax.swing.SwingUtilities;
039    import javax.swing.JPanel;
040    import javax.swing.JViewport;
041    import javax.swing.JScrollPane;
042    import javax.swing.JRootPane;
043    
044    /**
045     * A helper class that attaches one component (the overlay) on top of another
046     * component.
047     *
048     * @author oliverh
049     */
050    public class OverlayHelper implements SwingConstants
051    {
052        private final OverlayTargetChangeHandler overlayTargetChangeHandler = new OverlayTargetChangeHandler();
053    
054        private final OverlayChangeHandler overlayChangeHandler = new OverlayChangeHandler();
055    
056        protected final JComponent overlay;
057        protected final JComponent overlayClipper;
058        protected final JComponent overlayTarget;
059    
060        private final int center;
061    
062        private final int xOffset;
063    
064        private final int yOffset;
065    
066        boolean isUpdating;
067    
068        private Runnable overlayUpdater = new OverlayUpdater();
069    
070        /**
071         * Attaches an overlay to the specified component.
072         *
073         * @param overlay       the overlay component
074         * @param overlayTarget the component over which <code>overlay</code> will be
075         *                      attached
076         * @param center        position relative to <code>overlayTarget</code> that overlay
077         *                      should be centered. May be one of the
078         *                      <code>SwingConstants</code> compass positions or
079         *                      <code>SwingConstants.CENTER</code>.
080         * @param xOffset       x offset from center
081         * @param yOffset       y offset from center
082         * @see SwingConstants
083         */
084        public static void attachOverlay(JComponent overlay, JComponent overlayTarget, int center, int xOffset, int yOffset)
085        {
086            new OverlayHelper(overlay, overlayTarget, center, xOffset, yOffset);
087        }
088    
089        protected OverlayHelper(JComponent overlay, JComponent overlayTarget, int center, int xOffset, int yOffset)
090        {
091            this.overlay = overlay;
092            this.overlayTarget = overlayTarget;
093            this.center = center;
094            this.xOffset = xOffset;
095            this.yOffset = yOffset;
096            this.overlayClipper = new JPanel();
097            this.overlayClipper.setLayout(null);
098            this.overlayClipper.add(overlay);
099            this.overlayClipper.setOpaque(false);
100            installListeners();
101        }
102    
103        final class OverlayChangeHandler implements ComponentListener, PropertyChangeListener
104        {
105            public void componentHidden(ComponentEvent e)
106            {
107                hideOverlay();
108            }
109    
110            public void componentMoved(ComponentEvent e)
111            {
112                // ignore
113            }
114    
115            public void componentResized(ComponentEvent e)
116            {
117                // ignore
118            }
119    
120            public void componentShown(ComponentEvent e)
121            {
122                updateOverlay();
123            }
124    
125            public void propertyChange(PropertyChangeEvent e)
126            {
127                if ("ancestor".equals(e.getPropertyName()) || "layeredContainerLayer".equals(e.getPropertyName()))
128                {
129                    return;
130                }
131                updateOverlay();
132            }
133        }
134    
135        class OverlayTargetChangeHandler implements HierarchyListener, HierarchyBoundsListener, ComponentListener
136        {
137            public void hierarchyChanged(HierarchyEvent e)
138            {
139                updateOverlay();
140            }
141    
142            public void ancestorMoved(HierarchyEvent e)
143            {
144                updateOverlay();
145            }
146    
147            public void ancestorResized(HierarchyEvent e)
148            {
149                updateOverlay();
150            }
151    
152            public void componentHidden(ComponentEvent e)
153            {
154                hideOverlay();
155            }
156    
157            public void componentMoved(ComponentEvent e)
158            {
159                updateOverlay();
160            }
161    
162            public void componentResized(ComponentEvent e)
163            {
164                updateOverlay();
165            }
166    
167            public void componentShown(ComponentEvent e)
168            {
169                updateOverlay();
170            }
171        }
172    
173        private void installListeners()
174        {
175            overlayTarget.addHierarchyListener(overlayTargetChangeHandler);
176            overlayTarget.addHierarchyBoundsListener(overlayTargetChangeHandler);
177            overlayTarget.addComponentListener(overlayTargetChangeHandler);
178            overlay.addComponentListener(overlayChangeHandler);
179            overlay.addPropertyChangeListener(overlayChangeHandler);
180        }
181    
182        void updateOverlay()
183        {
184            if (isUpdating)
185            {
186                return;
187            }
188            isUpdating = true;
189            // updating the overlay at the end of the event queue to avoid race conditions 
190            // see RCP-126 (http://opensource.atlassian.com/projects/spring/browse/RCP-216)
191            SwingUtilities.invokeLater(overlayUpdater);
192        }
193    
194        void putOverlay(final JLayeredPane layeredPane)
195        {
196            if (overlay.getParent() != overlayClipper)
197            {
198                JComponent parent = (JComponent) overlay.getParent();
199                if (parent != null)
200                {
201                    parent.remove(overlay);
202                }
203                overlayClipper.add(overlay);
204            }
205            if (overlayClipper.getParent() != layeredPane)
206            {
207                JComponent parent = (JComponent) overlayClipper.getParent();
208                if (parent != null)
209                {
210                    parent.remove(overlayClipper);
211                }
212                layeredPane.add(overlayClipper);
213                layeredPane.setLayer(overlayClipper, JLayeredPane.PALETTE_LAYER.intValue());
214            }
215        }
216    
217        void positionOverlay(JLayeredPane layeredPane)
218        {
219            int centerX = xOffset;
220            int centerY = yOffset;
221            Rectangle overlayTargetBounds = new Rectangle(0, 0, overlayTarget.getWidth(), overlayTarget.getHeight());
222            switch (center)
223            {
224                case SwingConstants.NORTH:
225                case SwingConstants.NORTH_WEST:
226                case SwingConstants.NORTH_EAST:
227                    centerY += overlayTargetBounds.y;
228                    break;
229                case SwingConstants.CENTER:
230                case SwingConstants.EAST:
231                case SwingConstants.WEST:
232                    centerY += overlayTargetBounds.y + (overlayTargetBounds.height / 2);
233                    break;
234                case SwingConstants.SOUTH:
235                case SwingConstants.SOUTH_EAST:
236                case SwingConstants.SOUTH_WEST:
237                    centerY += overlayTargetBounds.y + overlayTargetBounds.height;
238                    break;
239                default:
240                    throw new IllegalArgumentException("Unknown value for center [" + center + "]");
241            }
242            switch (center)
243            {
244                case SwingConstants.WEST:
245                case SwingConstants.NORTH_WEST:
246                case SwingConstants.SOUTH_WEST:
247                    centerX += overlayTargetBounds.x;
248                    break;
249                case SwingConstants.CENTER:
250                case SwingConstants.NORTH:
251                case SwingConstants.SOUTH:
252                    centerX += overlayTargetBounds.x + (overlayTargetBounds.width / 2);
253                    break;
254                case SwingConstants.EAST:
255                case SwingConstants.NORTH_EAST:
256                case SwingConstants.SOUTH_EAST:
257                    centerX += overlayTargetBounds.x + overlayTargetBounds.width;
258                    break;
259                default:
260                    throw new IllegalArgumentException("Unknown value for center [" + center + "]");
261            }
262            Dimension size = overlay.getPreferredSize();
263            Rectangle newBound = new Rectangle(centerX - (size.width / 2), centerY - (size.height / 2), size.width,
264                    size.height);
265            Rectangle visibleRect = findLargestVisibleRectFor(newBound);
266    
267            int offsetx = 0;
268            int offsety = 0;
269    
270            if (visibleRect != null)
271            {
272                if (newBound.y < visibleRect.y)
273                {
274                    offsety += visibleRect.y - newBound.y;
275                }
276                if (newBound.x < visibleRect.x)
277                {
278                    offsetx += visibleRect.x - newBound.x;
279                }
280                newBound = newBound.intersection(visibleRect);
281            }
282            else
283            {
284                newBound.width = newBound.height = 0;
285            }
286            Point pt = SwingUtilities.convertPoint(overlayTarget, newBound.x, newBound.y, layeredPane);
287            newBound.x = pt.x;
288            newBound.y = pt.y;
289            setOverlayBounds(newBound, offsetx, offsety);
290        }
291    
292        /**
293         * Searches up the component hierarchy to find the largest possible visible
294         * rect that can enclose the entire rectangle.
295         *
296         * @param overlayRect rectangle whose largest enclosing visible rect to find
297         * @return largest enclosing visible rect for the specified rectangle
298         */
299        private Rectangle findLargestVisibleRectFor(final Rectangle overlayRect)
300        {
301            Rectangle visibleRect = null;
302            int curxoffset = 0;
303            int curyoffset = 0;
304            if (overlayTarget == null)
305            {
306                return null;
307            }
308    
309            JComponent comp = overlayTarget;
310            do
311            {
312    
313                visibleRect = comp.getVisibleRect();
314                visibleRect.x -= curxoffset;
315                visibleRect.y -= curyoffset;
316                if (visibleRect.contains(overlayRect))
317                {
318                    return visibleRect;
319                }
320                curxoffset += comp.getX();
321                curyoffset += comp.getY();
322    
323                comp = comp.getParent() instanceof JComponent ? (JComponent) comp.getParent() : null;
324            }
325            while (comp != null && !(comp instanceof JViewport) && !(comp instanceof JScrollPane));
326    
327    
328            return visibleRect;
329        }
330    
331        private void setOverlayBounds(Rectangle newBounds, int xoffset, int yoffset)
332        {
333            final Dimension preferred = overlay.getPreferredSize();
334            final Rectangle overlayBounds = new Rectangle(-xoffset, -yoffset, preferred.width, preferred.height);
335            if (!overlayBounds.equals(overlay.getBounds()))
336            {
337                overlay.setBounds(overlayBounds);
338            }
339            if (!newBounds.equals(overlayClipper.getBounds()))
340            {
341                overlayClipper.setBounds(newBounds);
342            }
343        }
344    
345        void hideOverlay()
346        {
347            setOverlayBounds(new Rectangle(0, 0, 0, 0), 0, 0);
348        }
349    
350        void removeOverlay()
351        {
352            if (overlay.getParent() != overlayClipper && overlay.getParent() != null)
353            {
354                overlay.getParent().remove(overlay);
355            }
356            if (overlayClipper.getParent() != null)
357            {
358                overlayClipper.getParent().remove(overlayClipper);
359            }
360        }
361    
362        private Container overlayCapableParent;
363    
364        protected Container getOverlayCapableParent(JComponent component)
365        {
366            //if (overlayCapableParent != null)
367            //    return overlayCapableParent;
368            Component overlayChild = component;
369            overlayCapableParent = component.getParent();
370            if (overlay instanceof MayHaveMessagableTab)
371            {
372                MessagableTabbedPane tabbedPane;
373                while (overlayCapableParent != null && !(overlayCapableParent instanceof JRootPane))
374                {
375                    if (overlayCapableParent instanceof MessagableTabbedPane)
376                    {
377                        tabbedPane = (MessagableTabbedPane) overlayCapableParent;
378                        int tabIndex = tabbedPane.indexOfComponent(overlayChild);
379                        ((MayHaveMessagableTab) overlay).setMessagableTab(tabbedPane, tabIndex);
380                    }
381    
382                    overlayChild = overlayCapableParent;
383                    overlayCapableParent = overlayCapableParent.getParent();
384                }
385            }
386            else
387            {
388                while (overlayCapableParent != null && !(overlayCapableParent instanceof JRootPane))
389                {
390                    overlayCapableParent = overlayCapableParent.getParent();
391                }
392            }
393            return overlayCapableParent;
394        }
395    
396        protected JLayeredPane getLayeredPane(Container overlayCapableParent)
397        {
398            if (overlayCapableParent instanceof JRootPane)
399            {
400                return ((JRootPane) overlayCapableParent).getLayeredPane();
401            }
402            else
403            {
404                throw new IllegalArgumentException("Don't know how to handle parent of type ["
405                        + overlayCapableParent.getClass().getName() + "].");
406            }
407        }
408    
409    
410        public static class SingleComponentLayoutManager implements LayoutManager
411        {
412            private Component singleComponent;
413    
414            public SingleComponentLayoutManager(Component singleComponent)
415            {
416                this.singleComponent = singleComponent;
417            }
418    
419            public void removeLayoutComponent(Component comp)
420            {
421            }
422    
423            public void addLayoutComponent(String name, Component comp)
424            {
425            }
426    
427            public void layoutContainer(Container parent)
428            {
429                // Fix 5/12/06 AlD: we don't need to base this on the
430                // preferred size of the singleComponent or the extentSize
431                // of the viewport because the viewport will have already resized
432                // the JLayeredPane and taken everything else into consideration.
433                // It will have also honored the Scrollable flags, which is
434                // something the original code here did not do.
435                singleComponent.setBounds(0, 0, parent.getWidth(), parent.getHeight());
436            }
437    
438            public Dimension minimumLayoutSize(Container parent)
439            {
440                return singleComponent.getMinimumSize();
441            }
442    
443            public Dimension preferredLayoutSize(Container parent)
444            {
445                return singleComponent.getPreferredSize();
446            }
447        }
448    
449        class OverlayUpdater implements Runnable
450        {
451    
452            public void run()
453            {
454                try
455                {
456                    Container overlayCapableParent = getOverlayCapableParent(overlayTarget);
457                    if (overlayCapableParent == null)
458                    {
459                        removeOverlay();
460                    }
461                    else if (!overlayTarget.isShowing() || !overlay.isVisible())
462                    {
463                        hideOverlay();
464                    }
465                    else
466                    {
467                        JLayeredPane layeredPane = getLayeredPane(overlayCapableParent);
468                        if (layeredPane.isVisible() && layeredPane.isShowing())
469                        {
470                            putOverlay(layeredPane);
471                            positionOverlay(layeredPane);
472                        }
473                    }
474                }
475                finally
476                {
477                    isUpdating = false;
478                }
479            }
480        }
481    }