001    /*
002     * $Id: ComponentPopup.java,v 1.11 2012/02/19 17:35:50 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
007     * Space Science and Engineering Center (SSEC)
008     * University of Wisconsin - Madison
009     * 1225 W. Dayton Street, Madison, WI 53706, USA
010     * https://www.ssec.wisc.edu/mcidas
011     * 
012     * All Rights Reserved
013     * 
014     * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015     * some McIDAS-V source code is based on IDV and VisAD source code.  
016     * 
017     * McIDAS-V is free software; you can redistribute it and/or modify
018     * it under the terms of the GNU Lesser Public License as published by
019     * the Free Software Foundation; either version 3 of the License, or
020     * (at your option) any later version.
021     * 
022     * McIDAS-V is distributed in the hope that it will be useful,
023     * but WITHOUT ANY WARRANTY; without even the implied warranty of
024     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025     * GNU Lesser Public License for more details.
026     * 
027     * You should have received a copy of the GNU Lesser Public License
028     * along with this program.  If not, see http://www.gnu.org/licenses.
029     */
030    
031    package edu.wisc.ssec.mcidasv.ui;
032    
033    import java.awt.BorderLayout;
034    import java.awt.Component;
035    import java.awt.Dimension;
036    import java.awt.DisplayMode;
037    import java.awt.FlowLayout;
038    import java.awt.GraphicsDevice;
039    import java.awt.GraphicsEnvironment;
040    import java.awt.MouseInfo;
041    import java.awt.Point;
042    import java.awt.PointerInfo;
043    import java.awt.event.ActionEvent;
044    import java.awt.event.ActionListener;
045    import java.awt.event.ComponentAdapter;
046    import java.awt.event.ComponentEvent;
047    import java.awt.event.MouseAdapter;
048    import java.awt.event.MouseEvent;
049    
050    import javax.swing.BorderFactory;
051    import javax.swing.JButton;
052    import javax.swing.JFrame;
053    import javax.swing.JTree;
054    import javax.swing.JWindow;
055    import javax.swing.SwingUtilities;
056    import javax.swing.border.BevelBorder;
057    import javax.swing.tree.DefaultMutableTreeNode;
058    import javax.swing.tree.DefaultTreeModel;
059    
060    /**
061     * A popup window that attaches itself to a parent and can display an 
062     * component without preventing user interaction like a <tt>JComboBox</tt>.
063     *   
064     * @author <a href="https://www.ssec.wisc.edu/cgi-bin/email_form.cgi?name=Flynn,%20Bruce">Bruce Flynn, SSEC</a>
065     *
066     */
067    public class ComponentPopup extends JWindow {
068    
069        private static final long serialVersionUID = 7394231585407030118L;
070    
071        /**
072         * Number of pixels to use to compensate for when the mouse is moved slowly
073         * thereby hiding this popup when between components.
074         */
075        private static final int FLUFF = 3;
076    
077        /**
078         * Get the calculated total screen size.
079         * 
080         * @return The dimensions of the screen on the default screen device.
081         */
082        protected static Dimension getScreenSize() {
083            GraphicsEnvironment genv = GraphicsEnvironment
084                .getLocalGraphicsEnvironment();
085            GraphicsDevice gdev = genv.getDefaultScreenDevice();
086            DisplayMode dmode = gdev.getDisplayMode();
087    
088            return new Dimension(dmode.getWidth(), dmode.getHeight());
089        }
090    
091        /**
092         * Does the component contain the screen relative point.
093         * 
094         * @param comp The component to check.
095         * @param point Screen relative point.
096         * @param fluff Size in pixels of the area added to both sides of the
097         *        component in the x and y directions and used for the contains
098         *        calculation.
099         * @return True if the the point lies in the area plus or minus the fluff
100         *         factor in either direction.
101         */
102        public boolean containsPoint(Component comp, Point point, int fluff) {
103            if (!comp.isVisible()) {
104                return false;
105            }
106            Point my = comp.getLocationOnScreen();
107            boolean containsX = point.x > my.x - FLUFF && point.x < my.x + getWidth() + FLUFF;
108            boolean containsY = point.y > my.y - FLUFF && point.y < my.y + getHeight() + FLUFF;
109            return containsX && containsY;
110        }
111    
112        /**
113         * Does the component contain the screen relative point.
114         * 
115         * @param comp The component to check.
116         * @param point Screen relative point.
117         * @return True if the the point lies in the same area occupied by the
118         *         component.
119         */
120        public boolean containsPoint(Component comp, Point point) {
121            return containsPoint(comp, point, 0);
122        }
123    
124        /**
125         * Determines if the mouse is on me.
126         */
127        private final MouseAdapter ourHideAdapter;
128    
129        /**
130         * Determines if the mouse is on my dad.
131         */
132        private final MouseAdapter parentsHideAdapter;
133    
134        /**
135         * What to do if the parent compoentn state changes.
136         */
137        private final ComponentAdapter parentsCompAdapter;
138    
139        private Component parent;
140    
141        /**
142         * Create an instance associated with the given parent.
143         * 
144         * @param parent The component to attach this instance to.
145         */
146        public ComponentPopup(Component parent) {
147            ourHideAdapter = new MouseAdapter() {
148    
149                @Override
150                public void mouseExited(MouseEvent evt) {
151                    PointerInfo info = MouseInfo.getPointerInfo();
152                    boolean onParent = containsPoint(ComponentPopup.this.parent,
153                        info.getLocation());
154    
155                    if (isVisible() && !onParent) {
156                        setVisible(false);
157                    }
158                }
159            };
160            parentsHideAdapter = new MouseAdapter() {
161    
162                @Override
163                public void mouseExited(MouseEvent evt) {
164                    PointerInfo info = MouseInfo.getPointerInfo();
165                    boolean onComponent = containsPoint(ComponentPopup.this,
166                        info.getLocation());
167                    if (isVisible() && !onComponent) {
168                        setVisible(false);
169                    }
170                }
171            };
172            parentsCompAdapter = new ComponentAdapter() {
173    
174                @Override
175                public void componentHidden(ComponentEvent evt) {
176                    setVisible(false);
177                }
178    
179                @Override
180                public void componentResized(ComponentEvent evt) {
181                    showPopup();
182                }
183            };
184            setParent(parent);
185        }
186    
187        /**
188         * Set our parent. If there is currently a parent remove the associated
189         * listeners and add them to the new parent.
190         * 
191         * @param comp
192         */
193        public void setParent(Component comp) {
194            if (parent != null) {
195                parent.removeMouseListener(parentsHideAdapter);
196                parent.removeComponentListener(parentsCompAdapter);
197            }
198    
199            parent = comp;
200            parent.addComponentListener(parentsCompAdapter);
201            parent.addMouseListener(parentsHideAdapter);
202        }
203    
204        /**
205         * Show this popup above the parent. It is not checked if the component will
206         * fit on the screen.
207         */
208        public void showAbove() {
209            Point loc = parent.getLocationOnScreen();
210            int x = loc.x;
211            int y = loc.y - getHeight();
212            showPopupAt(x, y);
213        }
214    
215        /**
216         * Show this popup below the parent. It is not checked if the component will
217         * fit on the screen.
218         */
219        public void showBelow() {
220            Point loc = parent.getLocationOnScreen();
221            int x = loc.x;
222            int y = loc.y + parent.getHeight();
223            showPopupAt(x, y);
224        }
225    
226        /**
227         * Do we fit between the top of the parent and the top edge of the screen.
228         * 
229         * @return True if we fit between the upper edge of our parent and the top
230         *         edge of the screen.
231         */
232        protected boolean fitsAbove() {
233            Point loc = parent.getLocationOnScreen();
234            int myH = getHeight();
235            return loc.y - myH > 0;
236        }
237    
238        /**
239         * Do we fit between the bottom of the parent and the edge of the screen.
240         * 
241         * @return True if we fit between the bottom edge of our parent and the
242         *         bottom edge of the screen.
243         */
244        protected boolean fitsBelow() {
245            Point loc = parent.getLocationOnScreen();
246            Dimension scr = getScreenSize();
247            int myH = getHeight();
248            return loc.y + parent.getHeight() + myH < scr.height;
249        }
250    
251        /**
252         * Show at the specified X and Y.
253         * 
254         * @param x
255         * @param y
256         */
257        public void showPopupAt(int x, int y) {
258            setLocation(x, y);
259            setVisible(true);
260        }
261    
262        /**
263         * Show this popup deciding whether to show it above or below the parent
264         * component.
265         */
266        public void showPopup() {
267            if (fitsBelow()) {
268                showBelow();
269            } else {
270                showAbove();
271            }
272        }
273    
274        /**
275         * Overridden to make sure our hide listeners are added to child components.
276         * 
277         * @see javax.swing.JWindow#addImpl(java.awt.Component, java.lang.Object, int)
278         */
279        protected void addImpl(Component comp, Object constraints, int index) {
280            super.addImpl(comp, constraints, index);
281            comp.addMouseListener(ourHideAdapter);
282        }
283    
284        /**
285         * Test method.
286         */
287        private static void createAndShowGui() {
288            DefaultMutableTreeNode root = new DefaultMutableTreeNode("ROOT");
289            DefaultTreeModel model = new DefaultTreeModel(root);
290            JTree tree = new JTree(model);
291            tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
292    
293            root.add(new DefaultMutableTreeNode("Child 1"));
294            root.add(new DefaultMutableTreeNode("Child 2"));
295            root.add(new DefaultMutableTreeNode("Child 3"));
296    
297            for (int i = 0; i < tree.getRowCount(); i++) {
298                tree.expandPath(tree.getPathForRow(i));
299            }
300            final JButton button = new JButton("Popup");
301            final ComponentPopup cp = new ComponentPopup(button);
302            cp.add(tree, BorderLayout.CENTER);
303            cp.pack();
304            button.addActionListener(new ActionListener() {
305                public void actionPerformed(ActionEvent evt) {
306                    cp.showPopup();
307                }
308            });
309    
310            JFrame frame = new JFrame("ComponentPopup");
311            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
312            frame.setLayout(new FlowLayout());
313            frame.add(button);
314            frame.pack();
315            frame.setVisible(true);
316        }
317    
318        /**
319         * Test method.
320         * 
321         * @param args
322         */
323        public static void main(String[] args) {
324            try {
325                javax.swing.UIManager.setLookAndFeel(javax.swing.UIManager
326                    .getCrossPlatformLookAndFeelClassName());
327            } catch (Exception e) {
328                e.printStackTrace();
329            }
330            SwingUtilities.invokeLater(new Runnable() {
331    
332                public void run() {
333                    createAndShowGui();
334                }
335            });
336        }
337    
338    }