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

Last change on this file since 12304 was 12304, checked in by michael2402, 7 years ago

Javadoc for public methods / classes in gui.util and gui.widgets

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