001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2023
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.ComponentAdapter;
042import java.awt.event.ComponentEvent;
043import java.awt.event.MouseAdapter;
044import java.awt.event.MouseEvent;
045
046import javax.swing.BorderFactory;
047import javax.swing.JButton;
048import javax.swing.JFrame;
049import javax.swing.JTree;
050import javax.swing.JWindow;
051import javax.swing.SwingUtilities;
052import javax.swing.border.BevelBorder;
053import javax.swing.tree.DefaultMutableTreeNode;
054import javax.swing.tree.DefaultTreeModel;
055
056import org.slf4j.Logger;
057import org.slf4j.LoggerFactory;
058
059/**
060 * A popup window that attaches itself to a parent and can display an 
061 * component without preventing user interaction like a {@code JComboBox}.
062 *   
063 * @author <a href="https://www.ssec.wisc.edu/cgi-bin/email_form.cgi?name=Flynn,%20Bruce">Bruce Flynn, SSEC</a>
064 *
065 */
066public class ComponentPopup extends JWindow {
067    
068    private static final Logger logger =
069        LoggerFactory.getLogger(ComponentPopup.class);
070    
071    private static final long serialVersionUID = 7394231585407030118L;
072
073    /**
074     * Number of pixels to use to compensate for when the mouse is moved slowly
075     * thereby hiding this popup when between components.
076     */
077    private static final int FLUFF = 3;
078
079    /**
080     * Get the calculated total screen size.
081     * 
082     * @return The dimensions of the screen on the default screen device.
083     */
084    protected static Dimension getScreenSize() {
085        GraphicsEnvironment genv = GraphicsEnvironment
086            .getLocalGraphicsEnvironment();
087        GraphicsDevice gdev = genv.getDefaultScreenDevice();
088        DisplayMode dmode = gdev.getDisplayMode();
089
090        return new Dimension(dmode.getWidth(), dmode.getHeight());
091    }
092
093    /**
094     * Does the component contain the screen relative point.
095     * 
096     * @param comp The component to check.
097     * @param point Screen relative point.
098     * @param fluff Size in pixels of the area added to both sides of the
099     *        component in the x and y directions and used for the contains
100     *        calculation.
101     * @return True if the the point lies in the area plus or minus the fluff
102     *         factor in either direction.
103     */
104    public boolean containsPoint(Component comp, Point point, int fluff) {
105        if (!comp.isVisible()) {
106            return false;
107        }
108        Point my = comp.getLocationOnScreen();
109        boolean containsX = point.x > my.x - FLUFF && point.x < my.x + getWidth() + FLUFF;
110        boolean containsY = point.y > my.y - FLUFF && point.y < my.y + getHeight() + FLUFF;
111        return containsX && containsY;
112    }
113
114    /**
115     * Does the component contain the screen relative point.
116     * 
117     * @param comp The component to check.
118     * @param point Screen relative point.
119     * @return True if the the point lies in the same area occupied by the
120     *         component.
121     */
122    public boolean containsPoint(Component comp, Point point) {
123        return containsPoint(comp, point, 0);
124    }
125
126    /**
127     * Determines if the mouse is on me.
128     */
129    private final MouseAdapter ourHideAdapter;
130
131    /**
132     * Determines if the mouse is on my dad.
133     */
134    private final MouseAdapter parentsHideAdapter;
135
136    /**
137     * What to do if the parent compoentn state changes.
138     */
139    private final ComponentAdapter parentsCompAdapter;
140
141    private Component parent;
142
143    /**
144     * Create an instance associated with the given parent.
145     * 
146     * @param parent The component to attach this instance to.
147     */
148    public ComponentPopup(Component parent) {
149        ourHideAdapter = new MouseAdapter() {
150
151            @Override
152            public void mouseExited(MouseEvent evt) {
153                PointerInfo info = MouseInfo.getPointerInfo();
154                boolean onParent = containsPoint(ComponentPopup.this.parent,
155                    info.getLocation());
156
157                if (isVisible() && !onParent) {
158                    setVisible(false);
159                }
160            }
161        };
162        parentsHideAdapter = new MouseAdapter() {
163
164            @Override
165            public void mouseExited(MouseEvent evt) {
166                PointerInfo info = MouseInfo.getPointerInfo();
167                boolean onComponent = containsPoint(ComponentPopup.this,
168                    info.getLocation());
169                if (isVisible() && !onComponent) {
170                    setVisible(false);
171                }
172            }
173        };
174        parentsCompAdapter = new ComponentAdapter() {
175
176            @Override
177            public void componentHidden(ComponentEvent evt) {
178                setVisible(false);
179            }
180
181            @Override
182            public void componentResized(ComponentEvent evt) {
183                showPopup();
184            }
185        };
186        setParent(parent);
187    }
188
189    /**
190     * Set our parent. If there is currently a parent remove the associated
191     * listeners and add them to the new parent.
192     * 
193     * @param comp
194     */
195    public void setParent(Component comp) {
196        if (parent != null) {
197            parent.removeMouseListener(parentsHideAdapter);
198            parent.removeComponentListener(parentsCompAdapter);
199        }
200
201        parent = comp;
202        parent.addComponentListener(parentsCompAdapter);
203        parent.addMouseListener(parentsHideAdapter);
204    }
205
206    /**
207     * Show this popup above the parent. It is not checked if the component will
208     * fit on the screen.
209     */
210    public void showAbove() {
211        Point loc = parent.getLocationOnScreen();
212        int x = loc.x;
213        int y = loc.y - getHeight();
214        showPopupAt(x, y);
215    }
216
217    /**
218     * Show this popup below the parent. It is not checked if the component will
219     * fit on the screen.
220     */
221    public void showBelow() {
222        Point loc = parent.getLocationOnScreen();
223        int x = loc.x;
224        int y = loc.y + parent.getHeight();
225        showPopupAt(x, y);
226    }
227
228    /**
229     * Do we fit between the top of the parent and the top edge of the screen.
230     * 
231     * @return True if we fit between the upper edge of our parent and the top
232     *         edge of the screen.
233     */
234    protected boolean fitsAbove() {
235        Point loc = parent.getLocationOnScreen();
236        int myH = getHeight();
237        return loc.y - myH > 0;
238    }
239
240    /**
241     * Do we fit between the bottom of the parent and the edge of the screen.
242     * 
243     * @return True if we fit between the bottom edge of our parent and the
244     *         bottom edge of the screen.
245     */
246    protected boolean fitsBelow() {
247        Point loc = parent.getLocationOnScreen();
248        Dimension scr = getScreenSize();
249        int myH = getHeight();
250        return loc.y + parent.getHeight() + myH < scr.height;
251    }
252
253    /**
254     * Show at the specified X and Y.
255     * 
256     * @param x
257     * @param y
258     */
259    public void showPopupAt(int x, int y) {
260        setLocation(x, y);
261        setVisible(true);
262    }
263
264    /**
265     * Show this popup deciding whether to show it above or below the parent
266     * component.
267     */
268    public void showPopup() {
269        if (fitsBelow()) {
270            showBelow();
271        } else {
272            showAbove();
273        }
274    }
275
276    /**
277     * Overridden to make sure our hide listeners are added to child components.
278     * 
279     * @see javax.swing.JWindow#addImpl(java.awt.Component, java.lang.Object, int)
280     */
281    protected void addImpl(Component comp, Object constraints, int index) {
282        super.addImpl(comp, constraints, index);
283        comp.addMouseListener(ourHideAdapter);
284    }
285
286    /**
287     * Test method.
288     */
289    private static void createAndShowGui() {
290        DefaultMutableTreeNode root = new DefaultMutableTreeNode("ROOT");
291        DefaultTreeModel model = new DefaultTreeModel(root);
292        JTree tree = new JTree(model);
293        tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
294
295        root.add(new DefaultMutableTreeNode("Child 1"));
296        root.add(new DefaultMutableTreeNode("Child 2"));
297        root.add(new DefaultMutableTreeNode("Child 3"));
298
299        for (int i = 0; i < tree.getRowCount(); i++) {
300            tree.expandPath(tree.getPathForRow(i));
301        }
302        final JButton button = new JButton("Popup");
303        final ComponentPopup cp = new ComponentPopup(button);
304        cp.add(tree, BorderLayout.CENTER);
305        cp.pack();
306        button.addActionListener(evt -> cp.showPopup());
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            logger.error("Problem changing LAF", e);
327        }
328        SwingUtilities.invokeLater(ComponentPopup::createAndShowGui);
329    }
330
331}