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