001 /*
002 * $Id: ComponentPopup.java,v 1.11 2012/02/19 17:35:50 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2012
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 *
012 * All Rights Reserved
013 *
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.
016 *
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 *
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
025 * GNU Lesser Public License for more details.
026 *
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program. If not, see http://www.gnu.org/licenses.
029 */
030
031 package edu.wisc.ssec.mcidasv.ui;
032
033 import java.awt.BorderLayout;
034 import java.awt.Component;
035 import java.awt.Dimension;
036 import java.awt.DisplayMode;
037 import java.awt.FlowLayout;
038 import java.awt.GraphicsDevice;
039 import java.awt.GraphicsEnvironment;
040 import java.awt.MouseInfo;
041 import java.awt.Point;
042 import java.awt.PointerInfo;
043 import java.awt.event.ActionEvent;
044 import java.awt.event.ActionListener;
045 import java.awt.event.ComponentAdapter;
046 import java.awt.event.ComponentEvent;
047 import java.awt.event.MouseAdapter;
048 import java.awt.event.MouseEvent;
049
050 import javax.swing.BorderFactory;
051 import javax.swing.JButton;
052 import javax.swing.JFrame;
053 import javax.swing.JTree;
054 import javax.swing.JWindow;
055 import javax.swing.SwingUtilities;
056 import javax.swing.border.BevelBorder;
057 import javax.swing.tree.DefaultMutableTreeNode;
058 import javax.swing.tree.DefaultTreeModel;
059
060 /**
061 * A popup window that attaches itself to a parent and can display an
062 * component without preventing user interaction like a <tt>JComboBox</tt>.
063 *
064 * @author <a href="https://www.ssec.wisc.edu/cgi-bin/email_form.cgi?name=Flynn,%20Bruce">Bruce Flynn, SSEC</a>
065 *
066 */
067 public class ComponentPopup extends JWindow {
068
069 private static final long serialVersionUID = 7394231585407030118L;
070
071 /**
072 * Number of pixels to use to compensate for when the mouse is moved slowly
073 * thereby hiding this popup when between components.
074 */
075 private static final int FLUFF = 3;
076
077 /**
078 * Get the calculated total screen size.
079 *
080 * @return The dimensions of the screen on the default screen device.
081 */
082 protected static Dimension getScreenSize() {
083 GraphicsEnvironment genv = GraphicsEnvironment
084 .getLocalGraphicsEnvironment();
085 GraphicsDevice gdev = genv.getDefaultScreenDevice();
086 DisplayMode dmode = gdev.getDisplayMode();
087
088 return new Dimension(dmode.getWidth(), dmode.getHeight());
089 }
090
091 /**
092 * Does the component contain the screen relative point.
093 *
094 * @param comp The component to check.
095 * @param point Screen relative point.
096 * @param fluff Size in pixels of the area added to both sides of the
097 * component in the x and y directions and used for the contains
098 * calculation.
099 * @return True if the the point lies in the area plus or minus the fluff
100 * factor in either direction.
101 */
102 public boolean containsPoint(Component comp, Point point, int fluff) {
103 if (!comp.isVisible()) {
104 return false;
105 }
106 Point my = comp.getLocationOnScreen();
107 boolean containsX = point.x > my.x - FLUFF && point.x < my.x + getWidth() + FLUFF;
108 boolean containsY = point.y > my.y - FLUFF && point.y < my.y + getHeight() + FLUFF;
109 return containsX && containsY;
110 }
111
112 /**
113 * Does the component contain the screen relative point.
114 *
115 * @param comp The component to check.
116 * @param point Screen relative point.
117 * @return True if the the point lies in the same area occupied by the
118 * component.
119 */
120 public boolean containsPoint(Component comp, Point point) {
121 return containsPoint(comp, point, 0);
122 }
123
124 /**
125 * Determines if the mouse is on me.
126 */
127 private final MouseAdapter ourHideAdapter;
128
129 /**
130 * Determines if the mouse is on my dad.
131 */
132 private final MouseAdapter parentsHideAdapter;
133
134 /**
135 * What to do if the parent compoentn state changes.
136 */
137 private final ComponentAdapter parentsCompAdapter;
138
139 private Component parent;
140
141 /**
142 * Create an instance associated with the given parent.
143 *
144 * @param parent The component to attach this instance to.
145 */
146 public ComponentPopup(Component parent) {
147 ourHideAdapter = new MouseAdapter() {
148
149 @Override
150 public void mouseExited(MouseEvent evt) {
151 PointerInfo info = MouseInfo.getPointerInfo();
152 boolean onParent = containsPoint(ComponentPopup.this.parent,
153 info.getLocation());
154
155 if (isVisible() && !onParent) {
156 setVisible(false);
157 }
158 }
159 };
160 parentsHideAdapter = new MouseAdapter() {
161
162 @Override
163 public void mouseExited(MouseEvent evt) {
164 PointerInfo info = MouseInfo.getPointerInfo();
165 boolean onComponent = containsPoint(ComponentPopup.this,
166 info.getLocation());
167 if (isVisible() && !onComponent) {
168 setVisible(false);
169 }
170 }
171 };
172 parentsCompAdapter = new ComponentAdapter() {
173
174 @Override
175 public void componentHidden(ComponentEvent evt) {
176 setVisible(false);
177 }
178
179 @Override
180 public void componentResized(ComponentEvent evt) {
181 showPopup();
182 }
183 };
184 setParent(parent);
185 }
186
187 /**
188 * Set our parent. If there is currently a parent remove the associated
189 * listeners and add them to the new parent.
190 *
191 * @param comp
192 */
193 public void setParent(Component comp) {
194 if (parent != null) {
195 parent.removeMouseListener(parentsHideAdapter);
196 parent.removeComponentListener(parentsCompAdapter);
197 }
198
199 parent = comp;
200 parent.addComponentListener(parentsCompAdapter);
201 parent.addMouseListener(parentsHideAdapter);
202 }
203
204 /**
205 * Show this popup above the parent. It is not checked if the component will
206 * fit on the screen.
207 */
208 public void showAbove() {
209 Point loc = parent.getLocationOnScreen();
210 int x = loc.x;
211 int y = loc.y - getHeight();
212 showPopupAt(x, y);
213 }
214
215 /**
216 * Show this popup below the parent. It is not checked if the component will
217 * fit on the screen.
218 */
219 public void showBelow() {
220 Point loc = parent.getLocationOnScreen();
221 int x = loc.x;
222 int y = loc.y + parent.getHeight();
223 showPopupAt(x, y);
224 }
225
226 /**
227 * Do we fit between the top of the parent and the top edge of the screen.
228 *
229 * @return True if we fit between the upper edge of our parent and the top
230 * edge of the screen.
231 */
232 protected boolean fitsAbove() {
233 Point loc = parent.getLocationOnScreen();
234 int myH = getHeight();
235 return loc.y - myH > 0;
236 }
237
238 /**
239 * Do we fit between the bottom of the parent and the edge of the screen.
240 *
241 * @return True if we fit between the bottom edge of our parent and the
242 * bottom edge of the screen.
243 */
244 protected boolean fitsBelow() {
245 Point loc = parent.getLocationOnScreen();
246 Dimension scr = getScreenSize();
247 int myH = getHeight();
248 return loc.y + parent.getHeight() + myH < scr.height;
249 }
250
251 /**
252 * Show at the specified X and Y.
253 *
254 * @param x
255 * @param y
256 */
257 public void showPopupAt(int x, int y) {
258 setLocation(x, y);
259 setVisible(true);
260 }
261
262 /**
263 * Show this popup deciding whether to show it above or below the parent
264 * component.
265 */
266 public void showPopup() {
267 if (fitsBelow()) {
268 showBelow();
269 } else {
270 showAbove();
271 }
272 }
273
274 /**
275 * Overridden to make sure our hide listeners are added to child components.
276 *
277 * @see javax.swing.JWindow#addImpl(java.awt.Component, java.lang.Object, int)
278 */
279 protected void addImpl(Component comp, Object constraints, int index) {
280 super.addImpl(comp, constraints, index);
281 comp.addMouseListener(ourHideAdapter);
282 }
283
284 /**
285 * Test method.
286 */
287 private static void createAndShowGui() {
288 DefaultMutableTreeNode root = new DefaultMutableTreeNode("ROOT");
289 DefaultTreeModel model = new DefaultTreeModel(root);
290 JTree tree = new JTree(model);
291 tree.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
292
293 root.add(new DefaultMutableTreeNode("Child 1"));
294 root.add(new DefaultMutableTreeNode("Child 2"));
295 root.add(new DefaultMutableTreeNode("Child 3"));
296
297 for (int i = 0; i < tree.getRowCount(); i++) {
298 tree.expandPath(tree.getPathForRow(i));
299 }
300 final JButton button = new JButton("Popup");
301 final ComponentPopup cp = new ComponentPopup(button);
302 cp.add(tree, BorderLayout.CENTER);
303 cp.pack();
304 button.addActionListener(new ActionListener() {
305 public void actionPerformed(ActionEvent evt) {
306 cp.showPopup();
307 }
308 });
309
310 JFrame frame = new JFrame("ComponentPopup");
311 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
312 frame.setLayout(new FlowLayout());
313 frame.add(button);
314 frame.pack();
315 frame.setVisible(true);
316 }
317
318 /**
319 * Test method.
320 *
321 * @param args
322 */
323 public static void main(String[] args) {
324 try {
325 javax.swing.UIManager.setLookAndFeel(javax.swing.UIManager
326 .getCrossPlatformLookAndFeelClassName());
327 } catch (Exception e) {
328 e.printStackTrace();
329 }
330 SwingUtilities.invokeLater(new Runnable() {
331
332 public void run() {
333 createAndShowGui();
334 }
335 });
336 }
337
338 }