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}