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; 064import org.openstreetmap.josm.tools.bugreport.BugReport; 065import org.openstreetmap.josm.tools.bugreport.ReportedException; 066 067/** 068 * basic gui utils 069 */ 070public final class GuiHelper { 071 072 private GuiHelper() { 073 // Hide default constructor for utils classes 074 } 075 076 /** 077 * disable / enable a component and all its child components 078 * @param root component 079 * @param enabled enabled state 080 */ 081 public static void setEnabledRec(Container root, boolean enabled) { 082 root.setEnabled(enabled); 083 Component[] children = root.getComponents(); 084 for (Component child : children) { 085 if (child instanceof Container) { 086 setEnabledRec((Container) child, enabled); 087 } else { 088 child.setEnabled(enabled); 089 } 090 } 091 } 092 093 public static void executeByMainWorkerInEDT(final Runnable task) { 094 Main.worker.submit(new Runnable() { 095 @Override 096 public void run() { 097 runInEDTAndWait(task); 098 } 099 }); 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 -> thickness=3.5px; 3.5 10 5 -> 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}