001/*
002 * $Id: ComponentPopup.java,v 1.9 2011/03/24 16:06:34 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
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
031package edu.wisc.ssec.mcidasv.ui;
032
033import java.awt.BorderLayout;
034import java.awt.Component;
035import java.awt.Dimension;
036import java.awt.DisplayMode;
037import java.awt.FlowLayout;
038import java.awt.GraphicsDevice;
039import java.awt.GraphicsEnvironment;
040import java.awt.MouseInfo;
041import java.awt.Point;
042import java.awt.PointerInfo;
043import java.awt.event.ActionEvent;
044import java.awt.event.ActionListener;
045import java.awt.event.ComponentAdapter;
046import java.awt.event.ComponentEvent;
047import java.awt.event.MouseAdapter;
048import java.awt.event.MouseEvent;
049
050import javax.swing.BorderFactory;
051import javax.swing.JButton;
052import javax.swing.JFrame;
053import javax.swing.JTree;
054import javax.swing.JWindow;
055import javax.swing.SwingUtilities;
056import javax.swing.border.BevelBorder;
057import javax.swing.tree.DefaultMutableTreeNode;
058import 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 */
067public class ComponentPopup extends JWindow {
068
069        private static final long serialVersionUID = 7394231585407030118L;
070        
071        /**
072         * Number of pixels to use to compenstate 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         * @return The dimensions of the screen on the default screen device.
080         */
081        protected static Dimension getScreenSize() {
082                GraphicsEnvironment genv = GraphicsEnvironment.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         * @param comp The component to check.
092         * @param point Screen relative point.
093         * @param fluff Size in pixels of the area added to both sides of the component
094         *  in the x and y directions and used for the contains calculation.
095         * @return True if the the point lies in the area plus or minus the fluff factor
096         *      in either direction. 
097         */
098        public boolean containsPoint(Component comp, Point point, int fluff) {
099                if (!comp.isVisible()) {
100                        return false;
101                }
102                Point my = comp.getLocationOnScreen();
103                boolean containsX = point.x > my.x - FLUFF && point.x < my.x + getWidth() + FLUFF;
104                boolean containsY = point.y > my.y - FLUFF && point.y < my.y + getHeight() + FLUFF;
105                return containsX && containsY;
106        }
107        
108        /**
109         * Does the component contain the screen relative point.
110         * @param comp The component to check.
111         * @param point Screen relative point.
112         * @return True if the the point lies in the same area occupied by the component.
113         */
114        public boolean containsPoint(Component comp, Point point) {
115                return containsPoint(comp, point, 0);
116        }
117        
118        /**
119         * Determines if the mouse is on me.
120         */
121        private final MouseAdapter ourHideAdapter;
122        /**
123         * Determines if the mouse is on my dad.
124         */
125        private final MouseAdapter parentsHideAdapter;
126        /**
127         * What to do if the parent compoentn state changes.
128         */
129        private final ComponentAdapter parentsCompAdapter;
130        private Component parent;
131        
132        /**
133         * Create an instance associated with the given parent.
134         * @param parent The component to attach this instance to.
135         */
136        public ComponentPopup(Component parent) {
137                ourHideAdapter = new MouseAdapter() {
138                        @Override
139                        public void mouseExited(MouseEvent evt) {
140                                PointerInfo info = MouseInfo.getPointerInfo();
141                                boolean onParent = containsPoint(
142                                        ComponentPopup.this.parent,
143                                        info.getLocation()
144                                );
145                                
146                                if (isVisible() && !onParent) {
147                                        setVisible(false);
148                                }
149                        }
150                };
151                parentsHideAdapter = new MouseAdapter() {
152                        @Override
153                        public void mouseExited(MouseEvent evt) {
154                                PointerInfo info = MouseInfo.getPointerInfo();
155                                boolean onComponent = containsPoint(
156                                        ComponentPopup.this,
157                                        info.getLocation()
158                                );
159                                if (isVisible() && !onComponent) {
160                                        setVisible(false);
161                                }
162                        }
163                };
164                parentsCompAdapter = new ComponentAdapter() {
165                        @Override
166                        public void componentHidden(ComponentEvent evt) {
167                                setVisible(false);
168                        }
169                        @Override
170                        public void componentResized(ComponentEvent evt) {
171                                showPopup();
172                        }
173                };
174                setParent(parent);
175        }
176        
177        /**
178         * Set our parent. If there is currently a parent remove the associated
179         * listeners and add them to the new parent.
180         * @param comp
181         */
182        public void setParent(Component comp) {
183                if (parent != null) {
184                        parent.removeMouseListener(parentsHideAdapter);
185                        parent.removeComponentListener(parentsCompAdapter);
186                }
187                
188                parent = comp;
189                parent.addComponentListener(parentsCompAdapter);
190                parent.addMouseListener(parentsHideAdapter);
191        }
192        
193        /**
194         * Show this popup above the parent. It is not checked if
195         * the component will fit on the screen.
196         */
197        public void showAbove() {
198                Point loc = parent.getLocationOnScreen();
199                int x = loc.x;
200                int y = loc.y - getHeight();
201                showPopupAt(x, y);
202        }
203        
204        /**
205         * Show this popup below the parent. It is not checked if
206         * the component will fit on the screen.
207         */
208        public void showBelow() {
209                Point loc = parent.getLocationOnScreen();
210                int x = loc.x;
211                int y = loc.y + parent.getHeight();
212                showPopupAt(x, y);
213        }
214        
215        /**
216         * Do we fit between the top of the parent and the top edge
217         * of the screen.
218         * @return True if we fit between the upper edge of our parent and 
219         *      the top edge of the screen.
220         */
221        protected boolean fitsAbove() {
222                Point loc = parent.getLocationOnScreen();
223                int myH = getHeight();
224                return loc.y - myH > 0;
225        }
226        
227        /**
228         * Do we fit between the bottom of the parent and the edge
229         * of the screen.
230         * @return True if we fit between the bottom edge of our parent and
231         *      the bottom edge of the screen.
232         */
233        protected boolean fitsBelow() {
234                Point loc = parent.getLocationOnScreen();
235                Dimension scr = getScreenSize();
236                int myH = getHeight();
237                return loc.y + parent.getHeight() + myH < scr.height;
238        }
239        
240        /**
241         * Show at the specified X and Y.
242         * @param x
243         * @param y
244         */
245        public void showPopupAt(int x, int y) {
246                setLocation(x, y);
247                setVisible(true);
248        }
249        
250        /**
251         * Show this popup deciding whether to show it above
252         * or below the parent component.
253         */
254        public void showPopup() {
255                if (fitsBelow()) {
256                        showBelow();
257                } else {
258                        showAbove();
259                }
260        }
261
262        /**
263         * Overridden to make sure our hide listeners are added to child components.
264         * @see javax.swing.JWindow#addImpl(java.awt.Component, java.lang.Object, int)
265         */
266        protected void addImpl(Component comp, Object constraints, int index) {
267                super.addImpl(comp, constraints, index);
268                comp.addMouseListener(ourHideAdapter);
269        }
270        
271        /**
272         * Test method.
273         */
274        private static void createAndShowGui() {
275                
276        DefaultMutableTreeNode root = new DefaultMutableTreeNode("ROOT");
277        DefaultTreeModel model = new DefaultTreeModel(root);
278        JTree tree = new JTree(model);
279        tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
280        
281        root.add(new DefaultMutableTreeNode("Child 1"));
282        root.add(new DefaultMutableTreeNode("Child 2"));
283        root.add(new DefaultMutableTreeNode("Child 3"));
284        
285        for (int i = 0; i < tree.getRowCount(); i++) {
286            tree.expandPath(tree.getPathForRow(i));
287        }
288        
289        final JButton button = new JButton("Popup");
290        final ComponentPopup cp = new ComponentPopup(button);
291        cp.add(tree, BorderLayout.CENTER);
292        cp.pack();
293        
294        button.addActionListener(new ActionListener() {
295                        public void actionPerformed(ActionEvent evt) {
296                                cp.showPopup();
297                        }
298        });
299        
300        JFrame frame = new JFrame("ComponentPopup");
301        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
302        frame.setLayout(new FlowLayout());
303        frame.add(button);
304        frame.pack();
305        frame.setVisible(true);
306        }
307        
308        /**
309         * Test method.
310         * @param args
311         */
312        public static void main(String[] args) {
313                try {
314                        javax.swing.UIManager.setLookAndFeel(
315                                        javax.swing.UIManager.getCrossPlatformLookAndFeelClassName());
316                } catch (Exception e) {
317                        e.printStackTrace();
318                }
319                SwingUtilities.invokeLater(new Runnable() {
320                        public void run() {
321                                createAndShowGui();
322                        }
323                });
324        }
325        
326}