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