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.MouseAdapter; 024import java.awt.event.MouseEvent; 025import java.awt.image.FilteredImageSource; 026import java.lang.reflect.InvocationTargetException; 027import java.util.Arrays; 028import java.util.Collection; 029import java.util.Enumeration; 030import java.util.EventObject; 031import java.util.Locale; 032import java.util.concurrent.Callable; 033import java.util.concurrent.ExecutionException; 034import java.util.concurrent.FutureTask; 035 036import javax.swing.GrayFilter; 037import javax.swing.ImageIcon; 038import javax.swing.JColorChooser; 039import javax.swing.JComponent; 040import javax.swing.JFileChooser; 041import javax.swing.JLabel; 042import javax.swing.JOptionPane; 043import javax.swing.JPanel; 044import javax.swing.JPopupMenu; 045import javax.swing.JScrollPane; 046import javax.swing.Scrollable; 047import javax.swing.SwingUtilities; 048import javax.swing.Timer; 049import javax.swing.ToolTipManager; 050import javax.swing.UIManager; 051import javax.swing.plaf.FontUIResource; 052 053import org.openstreetmap.josm.data.preferences.StrokeProperty; 054import org.openstreetmap.josm.gui.ExtendedDialog; 055import org.openstreetmap.josm.gui.MainApplication; 056import org.openstreetmap.josm.gui.widgets.AbstractFileChooser; 057import org.openstreetmap.josm.gui.widgets.HtmlPanel; 058import org.openstreetmap.josm.tools.CheckParameterUtil; 059import org.openstreetmap.josm.tools.ColorHelper; 060import org.openstreetmap.josm.tools.Destroyable; 061import org.openstreetmap.josm.tools.GBC; 062import org.openstreetmap.josm.tools.ImageOverlay; 063import org.openstreetmap.josm.tools.ImageProvider; 064import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 065import org.openstreetmap.josm.tools.LanguageInfo; 066import org.openstreetmap.josm.tools.Logging; 067import org.openstreetmap.josm.tools.bugreport.BugReport; 068import org.openstreetmap.josm.tools.bugreport.ReportedException; 069 070/** 071 * basic gui utils 072 */ 073public final class GuiHelper { 074 075 /* Localization keys for file chooser (and color chooser). */ 076 private static final String[] JAVA_INTERNAL_MESSAGE_KEYS = { 077 /* JFileChooser windows laf */ 078 "FileChooser.detailsViewActionLabelText", 079 "FileChooser.detailsViewButtonAccessibleName", 080 "FileChooser.detailsViewButtonToolTipText", 081 "FileChooser.fileAttrHeaderText", 082 "FileChooser.fileDateHeaderText", 083 "FileChooser.fileNameHeaderText", 084 "FileChooser.fileNameLabelText", 085 "FileChooser.fileSizeHeaderText", 086 "FileChooser.fileTypeHeaderText", 087 "FileChooser.filesOfTypeLabelText", 088 "FileChooser.homeFolderAccessibleName", 089 "FileChooser.homeFolderToolTipText", 090 "FileChooser.listViewActionLabelText", 091 "FileChooser.listViewButtonAccessibleName", 092 "FileChooser.listViewButtonToolTipText", 093 "FileChooser.lookInLabelText", 094 "FileChooser.newFolderAccessibleName", 095 "FileChooser.newFolderActionLabelText", 096 "FileChooser.newFolderToolTipText", 097 "FileChooser.refreshActionLabelText", 098 "FileChooser.saveInLabelText", 099 "FileChooser.upFolderAccessibleName", 100 "FileChooser.upFolderToolTipText", 101 "FileChooser.viewMenuLabelText", 102 103 /* JFileChooser gtk laf */ 104 "FileChooser.acceptAllFileFilterText", 105 "FileChooser.cancelButtonText", 106 "FileChooser.cancelButtonToolTipText", 107 "FileChooser.deleteFileButtonText", 108 "FileChooser.filesLabelText", 109 "FileChooser.filterLabelText", 110 "FileChooser.foldersLabelText", 111 "FileChooser.newFolderButtonText", 112 "FileChooser.newFolderDialogText", 113 "FileChooser.openButtonText", 114 "FileChooser.openButtonToolTipText", 115 "FileChooser.openDialogTitleText", 116 "FileChooser.pathLabelText", 117 "FileChooser.renameFileButtonText", 118 "FileChooser.renameFileDialogText", 119 "FileChooser.renameFileErrorText", 120 "FileChooser.renameFileErrorTitle", 121 "FileChooser.saveButtonText", 122 "FileChooser.saveButtonToolTipText", 123 "FileChooser.saveDialogTitleText", 124 125 /* JFileChooser motif laf */ 126 //"FileChooser.cancelButtonText", 127 //"FileChooser.cancelButtonToolTipText", 128 "FileChooser.enterFileNameLabelText", 129 //"FileChooser.filesLabelText", 130 //"FileChooser.filterLabelText", 131 //"FileChooser.foldersLabelText", 132 "FileChooser.helpButtonText", 133 "FileChooser.helpButtonToolTipText", 134 //"FileChooser.openButtonText", 135 //"FileChooser.openButtonToolTipText", 136 //"FileChooser.openDialogTitleText", 137 //"FileChooser.pathLabelText", 138 //"FileChooser.saveButtonText", 139 //"FileChooser.saveButtonToolTipText", 140 //"FileChooser.saveDialogTitleText", 141 "FileChooser.updateButtonText", 142 "FileChooser.updateButtonToolTipText", 143 144 /* gtk color chooser */ 145 "GTKColorChooserPanel.blueText", 146 "GTKColorChooserPanel.colorNameText", 147 "GTKColorChooserPanel.greenText", 148 "GTKColorChooserPanel.hueText", 149 "GTKColorChooserPanel.nameText", 150 "GTKColorChooserPanel.redText", 151 "GTKColorChooserPanel.saturationText", 152 "GTKColorChooserPanel.valueText", 153 154 /* JOptionPane */ 155 "OptionPane.okButtonText", 156 "OptionPane.yesButtonText", 157 "OptionPane.noButtonText", 158 "OptionPane.cancelButtonText" 159 }; 160 161 private GuiHelper() { 162 // Hide default constructor for utils classes 163 } 164 165 /** 166 * disable / enable a component and all its child components 167 * @param root component 168 * @param enabled enabled state 169 */ 170 public static void setEnabledRec(Container root, boolean enabled) { 171 root.setEnabled(enabled); 172 Component[] children = root.getComponents(); 173 for (Component child : children) { 174 if (child instanceof Container) { 175 setEnabledRec((Container) child, enabled); 176 } else { 177 child.setEnabled(enabled); 178 } 179 } 180 } 181 182 /** 183 * Add a task to the main worker that will block the worker and run in the GUI thread. 184 * @param task The task to run 185 */ 186 public static void executeByMainWorkerInEDT(final Runnable task) { 187 MainApplication.worker.submit(() -> runInEDTAndWait(task)); 188 } 189 190 /** 191 * Executes asynchronously a runnable in 192 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>, 193 * except if we're already in the EDT: in this case the runnable is executed synchronously. 194 * @param task The runnable to execute 195 * @see SwingUtilities#invokeLater 196 */ 197 public static void runInEDT(Runnable task) { 198 if (SwingUtilities.isEventDispatchThread()) { 199 task.run(); 200 } else { 201 SwingUtilities.invokeLater(task); 202 } 203 } 204 205 private static void handleEDTException(Throwable t) { 206 Logging.logWithStackTrace(Logging.LEVEL_ERROR, t, "Exception raised in EDT"); 207 } 208 209 /** 210 * Executes synchronously a runnable in 211 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 212 * @param task The runnable to execute 213 * @see SwingUtilities#invokeAndWait 214 */ 215 public static void runInEDTAndWait(Runnable task) { 216 if (SwingUtilities.isEventDispatchThread()) { 217 task.run(); 218 } else { 219 try { 220 SwingUtilities.invokeAndWait(task); 221 } catch (InterruptedException | InvocationTargetException e) { 222 handleEDTException(e); 223 } 224 } 225 } 226 227 /** 228 * Executes synchronously a runnable in 229 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 230 * <p> 231 * Passes on the exception that was thrown to the thread calling this. 232 * The exception is wrapped using a {@link ReportedException}. 233 * @param task The runnable to execute 234 * @see SwingUtilities#invokeAndWait 235 * @since 10271 236 */ 237 public static void runInEDTAndWaitWithException(Runnable task) { 238 if (SwingUtilities.isEventDispatchThread()) { 239 task.run(); 240 } else { 241 try { 242 SwingUtilities.invokeAndWait(task); 243 } catch (InterruptedException | InvocationTargetException e) { 244 throw BugReport.intercept(e).put("task", task); 245 } 246 } 247 } 248 249 /** 250 * Executes synchronously a callable in 251 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a> 252 * and return a value. 253 * @param <V> the result type of method <code>call</code> 254 * @param callable The callable to execute 255 * @return The computed result 256 * @since 7204 257 */ 258 public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) { 259 if (SwingUtilities.isEventDispatchThread()) { 260 try { 261 return callable.call(); 262 } catch (Exception e) { // NOPMD 263 handleEDTException(e); 264 return null; 265 } 266 } else { 267 FutureTask<V> task = new FutureTask<>(callable); 268 SwingUtilities.invokeLater(task); 269 try { 270 return task.get(); 271 } catch (InterruptedException | ExecutionException e) { 272 handleEDTException(e); 273 return null; 274 } 275 } 276 } 277 278 /** 279 * This function fails if it was not called from the EDT thread. 280 * @throws IllegalStateException if called from wrong thread. 281 * @since 10271 282 */ 283 public static void assertCallFromEdt() { 284 if (!SwingUtilities.isEventDispatchThread()) { 285 throw new IllegalStateException( 286 "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName()); 287 } 288 } 289 290 /** 291 * Warns user about a dangerous action requiring confirmation. 292 * @param title Title of dialog 293 * @param content Content of dialog 294 * @param baseActionIcon Unused? FIXME why is this parameter unused? 295 * @param continueToolTip Tooltip to display for "continue" button 296 * @return true if the user wants to cancel, false if they want to continue 297 */ 298 public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) { 299 ExtendedDialog dlg = new ExtendedDialog(MainApplication.getMainFrame(), 300 title, tr("Cancel"), tr("Continue")); 301 dlg.setContent(content); 302 dlg.setButtonIcons( 303 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(), 304 new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( 305 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()); 306 dlg.setToolTipTexts(tr("Cancel"), continueToolTip); 307 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 308 dlg.setCancelButton(1); 309 return dlg.showDialog().getValue() != 2; 310 } 311 312 /** 313 * Notifies user about an error received from an external source as an HTML page. 314 * @param parent Parent component 315 * @param title Title of dialog 316 * @param message Message displayed at the top of the dialog 317 * @param html HTML content to display (real error message) 318 * @since 7312 319 */ 320 public static void notifyUserHtmlError(Component parent, String title, String message, String html) { 321 JPanel p = new JPanel(new GridBagLayout()); 322 p.add(new JLabel(message), GBC.eol()); 323 p.add(new JLabel(tr("Received error page:")), GBC.eol()); 324 JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html)); 325 sp.setPreferredSize(new Dimension(640, 240)); 326 p.add(sp, GBC.eol().fill(GBC.BOTH)); 327 328 ExtendedDialog ed = new ExtendedDialog(parent, title, tr("OK")); 329 ed.setButtonIcons("ok"); 330 ed.setContent(p); 331 ed.showDialog(); 332 } 333 334 /** 335 * Replies the disabled (grayed) version of the specified image. 336 * @param image The image to disable 337 * @return The disabled (grayed) version of the specified image, brightened by 20%. 338 * @since 5484 339 */ 340 public static Image getDisabledImage(Image image) { 341 return Toolkit.getDefaultToolkit().createImage( 342 new FilteredImageSource(image.getSource(), new GrayFilter(true, 20))); 343 } 344 345 /** 346 * Replies the disabled (grayed) version of the specified icon. 347 * @param icon The icon to disable 348 * @return The disabled (grayed) version of the specified icon, brightened by 20%. 349 * @since 5484 350 */ 351 public static ImageIcon getDisabledIcon(ImageIcon icon) { 352 return new ImageIcon(getDisabledImage(icon.getImage())); 353 } 354 355 /** 356 * Attaches a {@code HierarchyListener} to the specified {@code Component} that 357 * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog 358 * to make it resizeable. 359 * @param pane The component that will be displayed 360 * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null 361 * @return {@code pane} 362 * @since 5493 363 */ 364 public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) { 365 if (pane != null) { 366 pane.addHierarchyListener(e -> { 367 Window window = SwingUtilities.getWindowAncestor(pane); 368 if (window instanceof Dialog) { 369 Dialog dialog = (Dialog) window; 370 if (!dialog.isResizable()) { 371 dialog.setResizable(true); 372 if (minDimension != null) { 373 dialog.setMinimumSize(minDimension); 374 } 375 } 376 } 377 }); 378 } 379 return pane; 380 } 381 382 /** 383 * Schedules a new Timer to be run in the future (once or several times). 384 * @param initialDelay milliseconds for the initial and between-event delay if repeatable 385 * @param actionListener an initial listener; can be null 386 * @param repeats specify false to make the timer stop after sending its first action event 387 * @return The (started) timer. 388 * @since 5735 389 */ 390 public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) { 391 Timer timer = new Timer(initialDelay, actionListener); 392 timer.setRepeats(repeats); 393 timer.start(); 394 return timer; 395 } 396 397 /** 398 * Return s new BasicStroke object with given thickness and style 399 * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty 400 * @return stroke for drawing 401 * @see StrokeProperty 402 */ 403 public static Stroke getCustomizedStroke(String code) { 404 return StrokeProperty.getFromString(code); 405 } 406 407 /** 408 * Gets the font used to display monospaced text in a component, if possible. 409 * @param component The component 410 * @return the font used to display monospaced text in a component, if possible 411 * @since 7896 412 */ 413 public static Font getMonospacedFont(JComponent component) { 414 // Special font for Khmer script 415 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 416 return component.getFont(); 417 } else { 418 return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize()); 419 } 420 } 421 422 /** 423 * Gets the font used to display JOSM title in about dialog and splash screen. 424 * @return title font 425 * @since 5797 426 */ 427 public static Font getTitleFont() { 428 return new Font("SansSerif", Font.BOLD, 23); 429 } 430 431 /** 432 * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}. 433 * @param panel The component to embed 434 * @return the vertical scrollable {@code JScrollPane} 435 * @since 6666 436 */ 437 public static JScrollPane embedInVerticalScrollPane(Component panel) { 438 return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 439 } 440 441 /** 442 * Set the default unit increment for a {@code JScrollPane}. 443 * 444 * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane} 445 * is a {@code JPanel} or other component that does not implement the {@link Scrollable} 446 * interface. 447 * The default unit increment is 1 pixel. Multiplied by the number of unit increments 448 * per mouse wheel "click" (platform dependent, usually 3), this makes a very 449 * sluggish mouse wheel experience. 450 * This methods sets the unit increment to a larger, more reasonable value. 451 * @param sp the scroll pane 452 * @return the scroll pane (same object) with fixed unit increment 453 * @throws IllegalArgumentException if the component inside of the scroll pane 454 * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer}, 455 * {@code JList}, {@code JTextComponent} and {@code JTable}) 456 */ 457 public static JScrollPane setDefaultIncrement(JScrollPane sp) { 458 if (sp.getViewport().getView() instanceof Scrollable) { 459 throw new IllegalArgumentException(); 460 } 461 sp.getVerticalScrollBar().setUnitIncrement(10); 462 sp.getHorizontalScrollBar().setUnitIncrement(10); 463 return sp; 464 } 465 466 /** 467 * Sets a global font for all UI, replacing default font of current look and feel. 468 * @param name Font name. It is up to the caller to make sure the font exists 469 * @throws IllegalArgumentException if name is null 470 * @since 7896 471 */ 472 public static void setUIFont(String name) { 473 CheckParameterUtil.ensureParameterNotNull(name, "name"); 474 Logging.info("Setting "+name+" as the default UI font"); 475 Enumeration<?> keys = UIManager.getDefaults().keys(); 476 while (keys.hasMoreElements()) { 477 Object key = keys.nextElement(); 478 Object value = UIManager.get(key); 479 if (value instanceof FontUIResource) { 480 FontUIResource fui = (FontUIResource) value; 481 UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize())); 482 } 483 } 484 } 485 486 /** 487 * Sets the background color for this component, and adjust the foreground color so the text remains readable. 488 * @param c component 489 * @param background background color 490 * @since 9223 491 */ 492 public static void setBackgroundReadable(JComponent c, Color background) { 493 c.setBackground(background); 494 c.setForeground(ColorHelper.getForegroundColor(background)); 495 } 496 497 /** 498 * Gets the size of the screen. On systems with multiple displays, the primary display is used. 499 * This method returns always 800x600 in headless mode (useful for unit tests). 500 * @return the size of this toolkit's screen, in pixels, or 800x600 501 * @see Toolkit#getScreenSize 502 * @since 9576 503 */ 504 public static Dimension getScreenSize() { 505 return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize(); 506 } 507 508 /** 509 * Gets the size of the screen. On systems with multiple displays, 510 * contrary to {@link #getScreenSize()}, the biggest display is used. 511 * This method returns always 800x600 in headless mode (useful for unit tests). 512 * @return the size of maximum screen, in pixels, or 800x600 513 * @see Toolkit#getScreenSize 514 * @since 10470 515 */ 516 public static Dimension getMaximumScreenSize() { 517 if (GraphicsEnvironment.isHeadless()) { 518 return new Dimension(800, 600); 519 } 520 521 int height = 0; 522 int width = 0; 523 for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { 524 DisplayMode dm = gd.getDisplayMode(); 525 height = Math.max(height, dm.getHeight()); 526 width = Math.max(width, dm.getWidth()); 527 } 528 if (height == 0 || width == 0) { 529 return new Dimension(800, 600); 530 } 531 return new Dimension(width, height); 532 } 533 534 /** 535 * Returns the first <code>Window</code> ancestor of event source, or 536 * {@code null} if event source is not a component contained inside a <code>Window</code>. 537 * @param e event object 538 * @return a Window, or {@code null} 539 * @since 9916 540 */ 541 public static Window getWindowAncestorFor(EventObject e) { 542 if (e != null) { 543 Object source = e.getSource(); 544 if (source instanceof Component) { 545 Window ancestor = SwingUtilities.getWindowAncestor((Component) source); 546 if (ancestor != null) { 547 return ancestor; 548 } else { 549 Container parent = ((Component) source).getParent(); 550 if (parent instanceof JPopupMenu) { 551 Component invoker = ((JPopupMenu) parent).getInvoker(); 552 return SwingUtilities.getWindowAncestor(invoker); 553 } 554 } 555 } 556 } 557 return null; 558 } 559 560 /** 561 * Extends tooltip dismiss delay to a default value of 1 minute for the given component. 562 * @param c component 563 * @since 10024 564 */ 565 public static void extendTooltipDelay(Component c) { 566 extendTooltipDelay(c, 60_000); 567 } 568 569 /** 570 * Extends tooltip dismiss delay to the specified value for the given component. 571 * @param c component 572 * @param delay tooltip dismiss delay in milliseconds 573 * @see <a href="http://stackoverflow.com/a/6517902/2257172">http://stackoverflow.com/a/6517902/2257172</a> 574 * @since 10024 575 */ 576 public static void extendTooltipDelay(Component c, final int delay) { 577 final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay(); 578 c.addMouseListener(new MouseAdapter() { 579 @Override 580 public void mouseEntered(MouseEvent me) { 581 ToolTipManager.sharedInstance().setDismissDelay(delay); 582 } 583 584 @Override 585 public void mouseExited(MouseEvent me) { 586 ToolTipManager.sharedInstance().setDismissDelay(defaultDismissTimeout); 587 } 588 }); 589 } 590 591 /** 592 * Returns the specified component's <code>Frame</code> without throwing exception in headless mode. 593 * 594 * @param parentComponent the <code>Component</code> to check for a <code>Frame</code> 595 * @return the <code>Frame</code> that contains the component, or <code>getRootFrame</code> 596 * if the component is <code>null</code>, or does not have a valid <code>Frame</code> parent 597 * @see JOptionPane#getFrameForComponent 598 * @see GraphicsEnvironment#isHeadless 599 * @since 10035 600 */ 601 public static Frame getFrameForComponent(Component parentComponent) { 602 try { 603 return JOptionPane.getFrameForComponent(parentComponent); 604 } catch (HeadlessException e) { 605 Logging.debug(e); 606 return null; 607 } 608 } 609 610 /** 611 * Localizations for file chooser dialog. 612 * For some locales (e.g. de, fr) translations are provided 613 * by Java, but not for others (e.g. ru, uk). 614 * @since 12644 (moved from I18n) 615 */ 616 public static void translateJavaInternalMessages() { 617 Locale l = Locale.getDefault(); 618 619 AbstractFileChooser.setDefaultLocale(l); 620 JFileChooser.setDefaultLocale(l); 621 JColorChooser.setDefaultLocale(l); 622 for (String key : JAVA_INTERNAL_MESSAGE_KEYS) { 623 String us = UIManager.getString(key, Locale.US); 624 String loc = UIManager.getString(key, l); 625 // only provide custom translation if it is not already localized by Java 626 if (us != null && us.equals(loc)) { 627 UIManager.put(key, tr(us)); 628 } 629 } 630 } 631 632 /** 633 * Setup special font for Khmer script, as the default Java fonts do not display these characters. 634 * @since 12644 (moved from I18n) 635 * @since 8282 636 */ 637 public static void setupLanguageFonts() { 638 // Use special font for Khmer script, as the default Java font do not display these characters 639 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 640 Collection<String> fonts = Arrays.asList( 641 GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); 642 for (String f : new String[]{"Khmer UI", "DaunPenh", "MoolBoran"}) { 643 if (fonts.contains(f)) { 644 setUIFont(f); 645 break; 646 } 647 } 648 } 649 } 650 651 /** 652 * Destroys recursively all {@link Destroyable} components of a given container, and optionnally the container itself. 653 * @param component the component to destroy 654 * @param destroyItself whether to destroy the component itself 655 * @since 14463 656 */ 657 public static void destroyComponents(Component component, boolean destroyItself) { 658 if (component instanceof Container) { 659 for (Component c: ((Container) component).getComponents()) { 660 destroyComponents(c, true); 661 } 662 } 663 if (destroyItself && component instanceof Destroyable) { 664 ((Destroyable) component).destroy(); 665 } 666 } 667}