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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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