source: josm/trunk/src/org/openstreetmap/josm/gui/util/GuiHelper.java@ 10471

Last change on this file since 10471 was 10471, checked in by Don-vip, 8 years ago

fix #13043 - NPE

  • Property svn:eol-style set to native
File size: 22.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.util;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BasicStroke;
7import java.awt.Color;
8import java.awt.Component;
9import java.awt.Container;
10import java.awt.Dialog;
11import java.awt.Dimension;
12import java.awt.DisplayMode;
13import java.awt.Font;
14import java.awt.Frame;
15import java.awt.GraphicsDevice;
16import java.awt.GraphicsEnvironment;
17import java.awt.GridBagLayout;
18import java.awt.HeadlessException;
19import java.awt.Image;
20import java.awt.Stroke;
21import java.awt.Toolkit;
22import java.awt.Window;
23import java.awt.datatransfer.Clipboard;
24import java.awt.event.ActionListener;
25import java.awt.event.HierarchyEvent;
26import java.awt.event.HierarchyListener;
27import java.awt.event.KeyEvent;
28import java.awt.event.MouseAdapter;
29import java.awt.event.MouseEvent;
30import java.awt.image.FilteredImageSource;
31import java.lang.reflect.InvocationTargetException;
32import java.util.Enumeration;
33import java.util.EventObject;
34import java.util.concurrent.Callable;
35import java.util.concurrent.ExecutionException;
36import java.util.concurrent.FutureTask;
37
38import javax.swing.GrayFilter;
39import javax.swing.Icon;
40import javax.swing.ImageIcon;
41import javax.swing.JComponent;
42import javax.swing.JLabel;
43import javax.swing.JOptionPane;
44import javax.swing.JPanel;
45import javax.swing.JPopupMenu;
46import javax.swing.JScrollPane;
47import javax.swing.Scrollable;
48import javax.swing.SwingUtilities;
49import javax.swing.Timer;
50import javax.swing.ToolTipManager;
51import javax.swing.UIManager;
52import javax.swing.plaf.FontUIResource;
53
54import org.openstreetmap.josm.Main;
55import org.openstreetmap.josm.gui.ExtendedDialog;
56import org.openstreetmap.josm.gui.widgets.HtmlPanel;
57import org.openstreetmap.josm.tools.CheckParameterUtil;
58import org.openstreetmap.josm.tools.ColorHelper;
59import org.openstreetmap.josm.tools.GBC;
60import org.openstreetmap.josm.tools.ImageOverlay;
61import org.openstreetmap.josm.tools.ImageProvider;
62import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
63import org.openstreetmap.josm.tools.LanguageInfo;
64import org.openstreetmap.josm.tools.bugreport.BugReport;
65import org.openstreetmap.josm.tools.bugreport.ReportedException;
66
67/**
68 * basic gui utils
69 */
70public final class GuiHelper {
71
72 private GuiHelper() {
73 // Hide default constructor for utils classes
74 }
75
76 /**
77 * disable / enable a component and all its child components
78 * @param root component
79 * @param enabled enabled state
80 */
81 public static void setEnabledRec(Container root, boolean enabled) {
82 root.setEnabled(enabled);
83 Component[] children = root.getComponents();
84 for (Component child : children) {
85 if (child instanceof Container) {
86 setEnabledRec((Container) child, enabled);
87 } else {
88 child.setEnabled(enabled);
89 }
90 }
91 }
92
93 public static void executeByMainWorkerInEDT(final Runnable task) {
94 Main.worker.submit(new Runnable() {
95 @Override
96 public void run() {
97 runInEDTAndWait(task);
98 }
99 });
100 }
101
102 /**
103 * Executes asynchronously a runnable in
104 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
105 * @param task The runnable to execute
106 * @see SwingUtilities#invokeLater
107 */
108 public static void runInEDT(Runnable task) {
109 if (SwingUtilities.isEventDispatchThread()) {
110 task.run();
111 } else {
112 SwingUtilities.invokeLater(task);
113 }
114 }
115
116 /**
117 * Executes synchronously a runnable in
118 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
119 * @param task The runnable to execute
120 * @see SwingUtilities#invokeAndWait
121 */
122 public static void runInEDTAndWait(Runnable task) {
123 if (SwingUtilities.isEventDispatchThread()) {
124 task.run();
125 } else {
126 try {
127 SwingUtilities.invokeAndWait(task);
128 } catch (InterruptedException | InvocationTargetException e) {
129 Main.error(e);
130 }
131 }
132 }
133
134 /**
135 * Executes synchronously a runnable in
136 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
137 * <p>
138 * Passes on the exception that was thrown to the thread calling this.
139 * The exception is wrapped using a {@link ReportedException}.
140 * @param task The runnable to execute
141 * @see SwingUtilities#invokeAndWait
142 * @since 10271
143 */
144 public static void runInEDTAndWaitWithException(Runnable task) {
145 if (SwingUtilities.isEventDispatchThread()) {
146 task.run();
147 } else {
148 try {
149 SwingUtilities.invokeAndWait(task);
150 } catch (InterruptedException | InvocationTargetException e) {
151 throw BugReport.intercept(e).put("task", task);
152 }
153 }
154 }
155
156 /**
157 * Executes synchronously a callable in
158 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>
159 * and return a value.
160 * @param <V> the result type of method <tt>call</tt>
161 * @param callable The callable to execute
162 * @return The computed result
163 * @since 7204
164 */
165 public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) {
166 if (SwingUtilities.isEventDispatchThread()) {
167 try {
168 return callable.call();
169 } catch (Exception e) {
170 Main.error(e);
171 return null;
172 }
173 } else {
174 FutureTask<V> task = new FutureTask<>(callable);
175 SwingUtilities.invokeLater(task);
176 try {
177 return task.get();
178 } catch (InterruptedException | ExecutionException e) {
179 Main.error(e);
180 return null;
181 }
182 }
183 }
184
185 /**
186 * This function fails if it was not called from the EDT thread.
187 * @throws IllegalStateException if called from wrong thread.
188 * @since 10271
189 */
190 public static void assertCallFromEdt() {
191 if (!SwingUtilities.isEventDispatchThread()) {
192 throw new IllegalStateException(
193 "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName());
194 }
195 }
196
197 /**
198 * Warns user about a dangerous action requiring confirmation.
199 * @param title Title of dialog
200 * @param content Content of dialog
201 * @param baseActionIcon Unused? FIXME why is this parameter unused?
202 * @param continueToolTip Tooltip to display for "continue" button
203 * @return true if the user wants to cancel, false if they want to continue
204 */
205 public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) {
206 ExtendedDialog dlg = new ExtendedDialog(Main.parent,
207 title, new String[] {tr("Cancel"), tr("Continue")});
208 dlg.setContent(content);
209 dlg.setButtonIcons(new Icon[] {
210 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(),
211 new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay(
212 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()});
213 dlg.setToolTipTexts(new String[] {
214 tr("Cancel"),
215 continueToolTip});
216 dlg.setIcon(JOptionPane.WARNING_MESSAGE);
217 dlg.setCancelButton(1);
218 return dlg.showDialog().getValue() != 2;
219 }
220
221 /**
222 * Notifies user about an error received from an external source as an HTML page.
223 * @param parent Parent component
224 * @param title Title of dialog
225 * @param message Message displayed at the top of the dialog
226 * @param html HTML content to display (real error message)
227 * @since 7312
228 */
229 public static void notifyUserHtmlError(Component parent, String title, String message, String html) {
230 JPanel p = new JPanel(new GridBagLayout());
231 p.add(new JLabel(message), GBC.eol());
232 p.add(new JLabel(tr("Received error page:")), GBC.eol());
233 JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html));
234 sp.setPreferredSize(new Dimension(640, 240));
235 p.add(sp, GBC.eol().fill(GBC.BOTH));
236
237 ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")});
238 ed.setButtonIcons(new String[] {"ok.png"});
239 ed.setContent(p);
240 ed.showDialog();
241 }
242
243 /**
244 * Replies the disabled (grayed) version of the specified image.
245 * @param image The image to disable
246 * @return The disabled (grayed) version of the specified image, brightened by 20%.
247 * @since 5484
248 */
249 public static Image getDisabledImage(Image image) {
250 return Toolkit.getDefaultToolkit().createImage(
251 new FilteredImageSource(image.getSource(), new GrayFilter(true, 20)));
252 }
253
254 /**
255 * Replies the disabled (grayed) version of the specified icon.
256 * @param icon The icon to disable
257 * @return The disabled (grayed) version of the specified icon, brightened by 20%.
258 * @since 5484
259 */
260 public static ImageIcon getDisabledIcon(ImageIcon icon) {
261 return new ImageIcon(getDisabledImage(icon.getImage()));
262 }
263
264 /**
265 * Attaches a {@code HierarchyListener} to the specified {@code Component} that
266 * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog
267 * to make it resizeable.
268 * @param pane The component that will be displayed
269 * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null
270 * @return {@code pane}
271 * @since 5493
272 */
273 public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) {
274 if (pane != null) {
275 pane.addHierarchyListener(new HierarchyListener() {
276 @Override
277 public void hierarchyChanged(HierarchyEvent e) {
278 Window window = SwingUtilities.getWindowAncestor(pane);
279 if (window instanceof Dialog) {
280 Dialog dialog = (Dialog) window;
281 if (!dialog.isResizable()) {
282 dialog.setResizable(true);
283 if (minDimension != null) {
284 dialog.setMinimumSize(minDimension);
285 }
286 }
287 }
288 }
289 });
290 }
291 return pane;
292 }
293
294 /**
295 * Schedules a new Timer to be run in the future (once or several times).
296 * @param initialDelay milliseconds for the initial and between-event delay if repeatable
297 * @param actionListener an initial listener; can be null
298 * @param repeats specify false to make the timer stop after sending its first action event
299 * @return The (started) timer.
300 * @since 5735
301 */
302 public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) {
303 Timer timer = new Timer(initialDelay, actionListener);
304 timer.setRepeats(repeats);
305 timer.start();
306 return timer;
307 }
308
309 /**
310 * Return s new BasicStroke object with given thickness and style
311 * @param code = 3.5 -&gt; thickness=3.5px; 3.5 10 5 -&gt; thickness=3.5px, dashed: 10px filled + 5px empty
312 * @return stroke for drawing
313 */
314 public static Stroke getCustomizedStroke(String code) {
315 String[] s = code.trim().split("[^\\.0-9]+");
316
317 if (s.length == 0) return new BasicStroke();
318 float w;
319 try {
320 w = Float.parseFloat(s[0]);
321 } catch (NumberFormatException ex) {
322 w = 1.0f;
323 }
324 if (s.length > 1) {
325 float[] dash = new float[s.length-1];
326 float sumAbs = 0;
327 try {
328 for (int i = 0; i < s.length-1; i++) {
329 dash[i] = Float.parseFloat(s[i+1]);
330 sumAbs += Math.abs(dash[i]);
331 }
332 } catch (NumberFormatException ex) {
333 Main.error("Error in stroke preference format: "+code);
334 dash = new float[]{5.0f};
335 }
336 if (sumAbs < 1e-1) {
337 Main.error("Error in stroke dash fomat (all zeros): "+code);
338 return new BasicStroke(w);
339 }
340 // dashed stroke
341 return new BasicStroke(w, BasicStroke.CAP_BUTT,
342 BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
343 } else {
344 if (w > 1) {
345 // thick stroke
346 return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
347 } else {
348 // thin stroke
349 return new BasicStroke(w);
350 }
351 }
352 }
353
354 /**
355 * Gets the font used to display monospaced text in a component, if possible.
356 * @param component The component
357 * @return the font used to display monospaced text in a component, if possible
358 * @since 7896
359 */
360 public static Font getMonospacedFont(JComponent component) {
361 // Special font for Khmer script
362 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) {
363 return component.getFont();
364 } else {
365 return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize());
366 }
367 }
368
369 /**
370 * Gets the font used to display JOSM title in about dialog and splash screen.
371 * @return title font
372 * @since 5797
373 */
374 public static Font getTitleFont() {
375 return new Font("SansSerif", Font.BOLD, 23);
376 }
377
378 /**
379 * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}.
380 * @param panel The component to embed
381 * @return the vertical scrollable {@code JScrollPane}
382 * @since 6666
383 */
384 public static JScrollPane embedInVerticalScrollPane(Component panel) {
385 return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
386 }
387
388 /**
389 * Set the default unit increment for a {@code JScrollPane}.
390 *
391 * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane}
392 * is a {@code JPanel} or other component that does not implement the {@link Scrollable}
393 * interface.
394 * The default unit increment is 1 pixel. Multiplied by the number of unit increments
395 * per mouse wheel "click" (platform dependent, usually 3), this makes a very
396 * sluggish mouse wheel experience.
397 * This methods sets the unit increment to a larger, more reasonable value.
398 * @param sp the scroll pane
399 * @return the scroll pane (same object) with fixed unit increment
400 * @throws IllegalArgumentException if the component inside of the scroll pane
401 * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer},
402 * {@code JList}, {@code JTextComponent} and {@code JTable})
403 */
404 public static JScrollPane setDefaultIncrement(JScrollPane sp) {
405 if (sp.getViewport().getView() instanceof Scrollable) {
406 throw new IllegalArgumentException();
407 }
408 sp.getVerticalScrollBar().setUnitIncrement(10);
409 sp.getHorizontalScrollBar().setUnitIncrement(10);
410 return sp;
411 }
412
413 /**
414 * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts.
415 * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but:
416 * <ul>
417 * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended
418 * modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li>
419 * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li>
420 * </ul>
421 * @return extended modifier key used as the appropriate accelerator key for menu shortcuts
422 * @since 7539
423 */
424 public static int getMenuShortcutKeyMaskEx() {
425 return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK;
426 }
427
428 /**
429 * Sets a global font for all UI, replacing default font of current look and feel.
430 * @param name Font name. It is up to the caller to make sure the font exists
431 * @throws IllegalArgumentException if name is null
432 * @since 7896
433 */
434 public static void setUIFont(String name) {
435 CheckParameterUtil.ensureParameterNotNull(name, "name");
436 Main.info("Setting "+name+" as the default UI font");
437 Enumeration<?> keys = UIManager.getDefaults().keys();
438 while (keys.hasMoreElements()) {
439 Object key = keys.nextElement();
440 Object value = UIManager.get(key);
441 if (value instanceof FontUIResource) {
442 FontUIResource fui = (FontUIResource) value;
443 UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize()));
444 }
445 }
446 }
447
448 /**
449 * Sets the background color for this component, and adjust the foreground color so the text remains readable.
450 * @param c component
451 * @param background background color
452 * @since 9223
453 */
454 public static void setBackgroundReadable(JComponent c, Color background) {
455 c.setBackground(background);
456 c.setForeground(ColorHelper.getForegroundColor(background));
457 }
458
459 /**
460 * Gets the size of the screen. On systems with multiple displays, the primary display is used.
461 * This method returns always 800x600 in headless mode (useful for unit tests).
462 * @return the size of this toolkit's screen, in pixels, or 800x600
463 * @see Toolkit#getScreenSize
464 * @since 9576
465 */
466 public static Dimension getScreenSize() {
467 return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize();
468 }
469
470 /**
471 * Gets the size of the screen. On systems with multiple displays,
472 * contrary to {@link #getScreenSize()}, the biggest display is used.
473 * This method returns always 800x600 in headless mode (useful for unit tests).
474 * @return the size of maximum screen, in pixels, or 800x600
475 * @see Toolkit#getScreenSize
476 * @since 10470
477 */
478 public static Dimension getMaximumScreenSize() {
479 if (GraphicsEnvironment.isHeadless()) {
480 return new Dimension(800, 600);
481 }
482
483 int height = 0;
484 int width = 0;
485 for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
486 DisplayMode dm = gd.getDisplayMode();
487 if (dm != null) {
488 height = Math.max(height, dm.getHeight());
489 width = Math.max(width, dm.getWidth());
490 }
491 }
492 if (height == 0 || width == 0) {
493 return new Dimension(800, 600);
494 }
495 return new Dimension(width, height);
496 }
497
498 /**
499 * Gets the singleton instance of the system selection as a <code>Clipboard</code> object.
500 * This allows an application to read and modify the current, system-wide selection.
501 * @return the system selection as a <code>Clipboard</code>, or <code>null</code> if the native platform does not
502 * support a system selection <code>Clipboard</code> or if GraphicsEnvironment.isHeadless() returns true
503 * @see Toolkit#getSystemSelection
504 * @since 9576
505 */
506 public static Clipboard getSystemSelection() {
507 return GraphicsEnvironment.isHeadless() ? null : Toolkit.getDefaultToolkit().getSystemSelection();
508 }
509
510 /**
511 * Returns the first <code>Window</code> ancestor of event source, or
512 * {@code null} if event source is not a component contained inside a <code>Window</code>.
513 * @param e event object
514 * @return a Window, or {@code null}
515 * @since 9916
516 */
517 public static Window getWindowAncestorFor(EventObject e) {
518 if (e != null) {
519 Object source = e.getSource();
520 if (source instanceof Component) {
521 Window ancestor = SwingUtilities.getWindowAncestor((Component) source);
522 if (ancestor != null) {
523 return ancestor;
524 } else {
525 Container parent = ((Component) source).getParent();
526 if (parent instanceof JPopupMenu) {
527 Component invoker = ((JPopupMenu) parent).getInvoker();
528 return SwingUtilities.getWindowAncestor(invoker);
529 }
530 }
531 }
532 }
533 return null;
534 }
535
536 /**
537 * Extends tooltip dismiss delay to a default value of 1 minute for the given component.
538 * @param c component
539 * @since 10024
540 */
541 public static void extendTooltipDelay(Component c) {
542 extendTooltipDelay(c, 60000);
543 }
544
545 /**
546 * Extends tooltip dismiss delay to the specified value for the given component.
547 * @param c component
548 * @param delay tooltip dismiss delay in milliseconds
549 * @see <a href="http://stackoverflow.com/a/6517902/2257172">http://stackoverflow.com/a/6517902/2257172</a>
550 * @since 10024
551 */
552 public static void extendTooltipDelay(Component c, final int delay) {
553 final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay();
554 c.addMouseListener(new MouseAdapter() {
555 @Override
556 public void mouseEntered(MouseEvent me) {
557 ToolTipManager.sharedInstance().setDismissDelay(delay);
558 }
559
560 @Override
561 public void mouseExited(MouseEvent me) {
562 ToolTipManager.sharedInstance().setDismissDelay(defaultDismissTimeout);
563 }
564 });
565 }
566
567 /**
568 * Returns the specified component's <code>Frame</code> without throwing exception in headless mode.
569 *
570 * @param parentComponent the <code>Component</code> to check for a <code>Frame</code>
571 * @return the <code>Frame</code> that contains the component, or <code>getRootFrame</code>
572 * if the component is <code>null</code>, or does not have a valid <code>Frame</code> parent
573 * @see JOptionPane#getFrameForComponent
574 * @see GraphicsEnvironment#isHeadless
575 * @since 10035
576 */
577 public static Frame getFrameForComponent(Component parentComponent) {
578 try {
579 return JOptionPane.getFrameForComponent(parentComponent);
580 } catch (HeadlessException e) {
581 Main.debug(e);
582 return null;
583 }
584 }
585}
Note: See TracBrowser for help on using the repository browser.