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