001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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}