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