001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.util; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Component; 008import java.awt.Container; 009import java.awt.Dialog; 010import java.awt.Dimension; 011import java.awt.DisplayMode; 012import java.awt.Font; 013import java.awt.Frame; 014import java.awt.GraphicsDevice; 015import java.awt.GraphicsEnvironment; 016import java.awt.GridBagLayout; 017import java.awt.HeadlessException; 018import java.awt.Image; 019import java.awt.Stroke; 020import java.awt.Toolkit; 021import java.awt.Window; 022import java.awt.event.ActionListener; 023import java.awt.event.KeyEvent; 024import java.awt.event.MouseAdapter; 025import java.awt.event.MouseEvent; 026import java.awt.image.FilteredImageSource; 027import java.lang.reflect.InvocationTargetException; 028import java.util.Enumeration; 029import java.util.EventObject; 030import java.util.concurrent.Callable; 031import java.util.concurrent.ExecutionException; 032import java.util.concurrent.FutureTask; 033 034import javax.swing.GrayFilter; 035import javax.swing.Icon; 036import javax.swing.ImageIcon; 037import javax.swing.JComponent; 038import javax.swing.JLabel; 039import javax.swing.JOptionPane; 040import javax.swing.JPanel; 041import javax.swing.JPopupMenu; 042import javax.swing.JScrollPane; 043import javax.swing.Scrollable; 044import javax.swing.SwingUtilities; 045import javax.swing.Timer; 046import javax.swing.ToolTipManager; 047import javax.swing.UIManager; 048import javax.swing.plaf.FontUIResource; 049 050import org.openstreetmap.josm.Main; 051import org.openstreetmap.josm.data.preferences.StrokeProperty; 052import org.openstreetmap.josm.gui.ExtendedDialog; 053import org.openstreetmap.josm.gui.widgets.HtmlPanel; 054import org.openstreetmap.josm.tools.CheckParameterUtil; 055import org.openstreetmap.josm.tools.ColorHelper; 056import org.openstreetmap.josm.tools.GBC; 057import org.openstreetmap.josm.tools.ImageOverlay; 058import org.openstreetmap.josm.tools.ImageProvider; 059import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 060import org.openstreetmap.josm.tools.LanguageInfo; 061import org.openstreetmap.josm.tools.bugreport.BugReport; 062import org.openstreetmap.josm.tools.bugreport.ReportedException; 063 064/** 065 * basic gui utils 066 */ 067public final class GuiHelper { 068 069 private GuiHelper() { 070 // Hide default constructor for utils classes 071 } 072 073 /** 074 * disable / enable a component and all its child components 075 * @param root component 076 * @param enabled enabled state 077 */ 078 public static void setEnabledRec(Container root, boolean enabled) { 079 root.setEnabled(enabled); 080 Component[] children = root.getComponents(); 081 for (Component child : children) { 082 if (child instanceof Container) { 083 setEnabledRec((Container) child, enabled); 084 } else { 085 child.setEnabled(enabled); 086 } 087 } 088 } 089 090 public static void executeByMainWorkerInEDT(final Runnable task) { 091 Main.worker.submit(() -> runInEDTAndWait(task)); 092 } 093 094 /** 095 * Executes asynchronously a runnable in 096 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 097 * @param task The runnable to execute 098 * @see SwingUtilities#invokeLater 099 */ 100 public static void runInEDT(Runnable task) { 101 if (SwingUtilities.isEventDispatchThread()) { 102 task.run(); 103 } else { 104 SwingUtilities.invokeLater(task); 105 } 106 } 107 108 /** 109 * Executes synchronously a runnable in 110 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 111 * @param task The runnable to execute 112 * @see SwingUtilities#invokeAndWait 113 */ 114 public static void runInEDTAndWait(Runnable task) { 115 if (SwingUtilities.isEventDispatchThread()) { 116 task.run(); 117 } else { 118 try { 119 SwingUtilities.invokeAndWait(task); 120 } catch (InterruptedException | InvocationTargetException e) { 121 Main.error(e); 122 } 123 } 124 } 125 126 /** 127 * Executes synchronously a runnable in 128 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 129 * <p> 130 * Passes on the exception that was thrown to the thread calling this. 131 * The exception is wrapped using a {@link ReportedException}. 132 * @param task The runnable to execute 133 * @see SwingUtilities#invokeAndWait 134 * @since 10271 135 */ 136 public static void runInEDTAndWaitWithException(Runnable task) { 137 if (SwingUtilities.isEventDispatchThread()) { 138 task.run(); 139 } else { 140 try { 141 SwingUtilities.invokeAndWait(task); 142 } catch (InterruptedException | InvocationTargetException e) { 143 throw BugReport.intercept(e).put("task", task); 144 } 145 } 146 } 147 148 /** 149 * Executes synchronously a callable in 150 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a> 151 * and return a value. 152 * @param <V> the result type of method <tt>call</tt> 153 * @param callable The callable to execute 154 * @return The computed result 155 * @since 7204 156 */ 157 public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) { 158 if (SwingUtilities.isEventDispatchThread()) { 159 try { 160 return callable.call(); 161 } catch (Exception e) { 162 Main.error(e); 163 return null; 164 } 165 } else { 166 FutureTask<V> task = new FutureTask<>(callable); 167 SwingUtilities.invokeLater(task); 168 try { 169 return task.get(); 170 } catch (InterruptedException | ExecutionException e) { 171 Main.error(e); 172 return null; 173 } 174 } 175 } 176 177 /** 178 * This function fails if it was not called from the EDT thread. 179 * @throws IllegalStateException if called from wrong thread. 180 * @since 10271 181 */ 182 public static void assertCallFromEdt() { 183 if (!SwingUtilities.isEventDispatchThread()) { 184 throw new IllegalStateException( 185 "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName()); 186 } 187 } 188 189 /** 190 * Warns user about a dangerous action requiring confirmation. 191 * @param title Title of dialog 192 * @param content Content of dialog 193 * @param baseActionIcon Unused? FIXME why is this parameter unused? 194 * @param continueToolTip Tooltip to display for "continue" button 195 * @return true if the user wants to cancel, false if they want to continue 196 */ 197 public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) { 198 ExtendedDialog dlg = new ExtendedDialog(Main.parent, 199 title, new String[] {tr("Cancel"), tr("Continue")}); 200 dlg.setContent(content); 201 dlg.setButtonIcons(new Icon[] { 202 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(), 203 new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( 204 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()}); 205 dlg.setToolTipTexts(new String[] { 206 tr("Cancel"), 207 continueToolTip}); 208 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 209 dlg.setCancelButton(1); 210 return dlg.showDialog().getValue() != 2; 211 } 212 213 /** 214 * Notifies user about an error received from an external source as an HTML page. 215 * @param parent Parent component 216 * @param title Title of dialog 217 * @param message Message displayed at the top of the dialog 218 * @param html HTML content to display (real error message) 219 * @since 7312 220 */ 221 public static void notifyUserHtmlError(Component parent, String title, String message, String html) { 222 JPanel p = new JPanel(new GridBagLayout()); 223 p.add(new JLabel(message), GBC.eol()); 224 p.add(new JLabel(tr("Received error page:")), GBC.eol()); 225 JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html)); 226 sp.setPreferredSize(new Dimension(640, 240)); 227 p.add(sp, GBC.eol().fill(GBC.BOTH)); 228 229 ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")}); 230 ed.setButtonIcons(new String[] {"ok.png"}); 231 ed.setContent(p); 232 ed.showDialog(); 233 } 234 235 /** 236 * Replies the disabled (grayed) version of the specified image. 237 * @param image The image to disable 238 * @return The disabled (grayed) version of the specified image, brightened by 20%. 239 * @since 5484 240 */ 241 public static Image getDisabledImage(Image image) { 242 return Toolkit.getDefaultToolkit().createImage( 243 new FilteredImageSource(image.getSource(), new GrayFilter(true, 20))); 244 } 245 246 /** 247 * Replies the disabled (grayed) version of the specified icon. 248 * @param icon The icon to disable 249 * @return The disabled (grayed) version of the specified icon, brightened by 20%. 250 * @since 5484 251 */ 252 public static ImageIcon getDisabledIcon(ImageIcon icon) { 253 return new ImageIcon(getDisabledImage(icon.getImage())); 254 } 255 256 /** 257 * Attaches a {@code HierarchyListener} to the specified {@code Component} that 258 * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog 259 * to make it resizeable. 260 * @param pane The component that will be displayed 261 * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null 262 * @return {@code pane} 263 * @since 5493 264 */ 265 public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) { 266 if (pane != null) { 267 pane.addHierarchyListener(e -> { 268 Window window = SwingUtilities.getWindowAncestor(pane); 269 if (window instanceof Dialog) { 270 Dialog dialog = (Dialog) window; 271 if (!dialog.isResizable()) { 272 dialog.setResizable(true); 273 if (minDimension != null) { 274 dialog.setMinimumSize(minDimension); 275 } 276 } 277 } 278 }); 279 } 280 return pane; 281 } 282 283 /** 284 * Schedules a new Timer to be run in the future (once or several times). 285 * @param initialDelay milliseconds for the initial and between-event delay if repeatable 286 * @param actionListener an initial listener; can be null 287 * @param repeats specify false to make the timer stop after sending its first action event 288 * @return The (started) timer. 289 * @since 5735 290 */ 291 public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) { 292 Timer timer = new Timer(initialDelay, actionListener); 293 timer.setRepeats(repeats); 294 timer.start(); 295 return timer; 296 } 297 298 /** 299 * Return s new BasicStroke object with given thickness and style 300 * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty 301 * @return stroke for drawing 302 * @see StrokeProperty 303 */ 304 public static Stroke getCustomizedStroke(String code) { 305 return StrokeProperty.getFromString(code); 306 } 307 308 /** 309 * Gets the font used to display monospaced text in a component, if possible. 310 * @param component The component 311 * @return the font used to display monospaced text in a component, if possible 312 * @since 7896 313 */ 314 public static Font getMonospacedFont(JComponent component) { 315 // Special font for Khmer script 316 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 317 return component.getFont(); 318 } else { 319 return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize()); 320 } 321 } 322 323 /** 324 * Gets the font used to display JOSM title in about dialog and splash screen. 325 * @return title font 326 * @since 5797 327 */ 328 public static Font getTitleFont() { 329 return new Font("SansSerif", Font.BOLD, 23); 330 } 331 332 /** 333 * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}. 334 * @param panel The component to embed 335 * @return the vertical scrollable {@code JScrollPane} 336 * @since 6666 337 */ 338 public static JScrollPane embedInVerticalScrollPane(Component panel) { 339 return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 340 } 341 342 /** 343 * Set the default unit increment for a {@code JScrollPane}. 344 * 345 * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane} 346 * is a {@code JPanel} or other component that does not implement the {@link Scrollable} 347 * interface. 348 * The default unit increment is 1 pixel. Multiplied by the number of unit increments 349 * per mouse wheel "click" (platform dependent, usually 3), this makes a very 350 * sluggish mouse wheel experience. 351 * This methods sets the unit increment to a larger, more reasonable value. 352 * @param sp the scroll pane 353 * @return the scroll pane (same object) with fixed unit increment 354 * @throws IllegalArgumentException if the component inside of the scroll pane 355 * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer}, 356 * {@code JList}, {@code JTextComponent} and {@code JTable}) 357 */ 358 public static JScrollPane setDefaultIncrement(JScrollPane sp) { 359 if (sp.getViewport().getView() instanceof Scrollable) { 360 throw new IllegalArgumentException(); 361 } 362 sp.getVerticalScrollBar().setUnitIncrement(10); 363 sp.getHorizontalScrollBar().setUnitIncrement(10); 364 return sp; 365 } 366 367 /** 368 * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts. 369 * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but: 370 * <ul> 371 * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended 372 * modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li> 373 * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li> 374 * </ul> 375 * @return extended modifier key used as the appropriate accelerator key for menu shortcuts 376 * @since 7539 377 */ 378 public static int getMenuShortcutKeyMaskEx() { 379 return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; 380 } 381 382 /** 383 * Sets a global font for all UI, replacing default font of current look and feel. 384 * @param name Font name. It is up to the caller to make sure the font exists 385 * @throws IllegalArgumentException if name is null 386 * @since 7896 387 */ 388 public static void setUIFont(String name) { 389 CheckParameterUtil.ensureParameterNotNull(name, "name"); 390 Main.info("Setting "+name+" as the default UI font"); 391 Enumeration<?> keys = UIManager.getDefaults().keys(); 392 while (keys.hasMoreElements()) { 393 Object key = keys.nextElement(); 394 Object value = UIManager.get(key); 395 if (value instanceof FontUIResource) { 396 FontUIResource fui = (FontUIResource) value; 397 UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize())); 398 } 399 } 400 } 401 402 /** 403 * Sets the background color for this component, and adjust the foreground color so the text remains readable. 404 * @param c component 405 * @param background background color 406 * @since 9223 407 */ 408 public static void setBackgroundReadable(JComponent c, Color background) { 409 c.setBackground(background); 410 c.setForeground(ColorHelper.getForegroundColor(background)); 411 } 412 413 /** 414 * Gets the size of the screen. On systems with multiple displays, the primary display is used. 415 * This method returns always 800x600 in headless mode (useful for unit tests). 416 * @return the size of this toolkit's screen, in pixels, or 800x600 417 * @see Toolkit#getScreenSize 418 * @since 9576 419 */ 420 public static Dimension getScreenSize() { 421 return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize(); 422 } 423 424 /** 425 * Gets the size of the screen. On systems with multiple displays, 426 * contrary to {@link #getScreenSize()}, the biggest display is used. 427 * This method returns always 800x600 in headless mode (useful for unit tests). 428 * @return the size of maximum screen, in pixels, or 800x600 429 * @see Toolkit#getScreenSize 430 * @since 10470 431 */ 432 public static Dimension getMaximumScreenSize() { 433 if (GraphicsEnvironment.isHeadless()) { 434 return new Dimension(800, 600); 435 } 436 437 int height = 0; 438 int width = 0; 439 for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { 440 DisplayMode dm = gd.getDisplayMode(); 441 if (dm != null) { 442 height = Math.max(height, dm.getHeight()); 443 width = Math.max(width, dm.getWidth()); 444 } 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 Main.debug(e); 524 return null; 525 } 526 } 527}