001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
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
029package edu.wisc.ssec.mcidasv.ui;
030
031import java.awt.BorderLayout;
032import java.awt.Component;
033import java.awt.Dimension;
034import java.awt.DisplayMode;
035import java.awt.FlowLayout;
036import java.awt.GraphicsDevice;
037import java.awt.GraphicsEnvironment;
038import java.awt.MouseInfo;
039import java.awt.Point;
040import java.awt.PointerInfo;
041import java.awt.event.ActionEvent;
042import java.awt.event.ActionListener;
043import java.awt.event.ComponentAdapter;
044import java.awt.event.ComponentEvent;
045import java.awt.event.MouseAdapter;
046import java.awt.event.MouseEvent;
047
048import javax.swing.BorderFactory;
049import javax.swing.JButton;
050import javax.swing.JFrame;
051import javax.swing.JTree;
052import javax.swing.JWindow;
053import javax.swing.SwingUtilities;
054import javax.swing.border.BevelBorder;
055import javax.swing.tree.DefaultMutableTreeNode;
056import 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 {@code JComboBox}.
061 *   
062 * @author <a href="https://www.ssec.wisc.edu/cgi-bin/email_form.cgi?name=Flynn,%20Bruce">Bruce Flynn, SSEC</a>
063 *
064 */
065public 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}