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 = new String[] { 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 * @param task The runnable to execute 194 * @see SwingUtilities#invokeLater 195 */ 196 public static void runInEDT(Runnable task) { 197 if (SwingUtilities.isEventDispatchThread()) { 198 task.run(); 199 } else { 200 SwingUtilities.invokeLater(task); 201 } 202 } 203 204 private static void handleEDTException(Throwable t) { 205 Logging.logWithStackTrace(Logging.LEVEL_ERROR, t, "Exception raised in EDT"); 206 } 207 208 /** 209 * Executes synchronously a runnable in 210 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 211 * @param task The runnable to execute 212 * @see SwingUtilities#invokeAndWait 213 */ 214 public static void runInEDTAndWait(Runnable task) { 215 if (SwingUtilities.isEventDispatchThread()) { 216 task.run(); 217 } else { 218 try { 219 SwingUtilities.invokeAndWait(task); 220 } catch (InterruptedException | InvocationTargetException e) { 221 handleEDTException(e); 222 } 223 } 224 } 225 226 /** 227 * Executes synchronously a runnable in 228 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 229 * <p> 230 * Passes on the exception that was thrown to the thread calling this. 231 * The exception is wrapped using a {@link ReportedException}. 232 * @param task The runnable to execute 233 * @see SwingUtilities#invokeAndWait 234 * @since 10271 235 */ 236 public static void runInEDTAndWaitWithException(Runnable task) { 237 if (SwingUtilities.isEventDispatchThread()) { 238 task.run(); 239 } else { 240 try { 241 SwingUtilities.invokeAndWait(task); 242 } catch (InterruptedException | InvocationTargetException e) { 243 throw BugReport.intercept(e).put("task", task); 244 } 245 } 246 } 247 248 /** 249 * Executes synchronously a callable in 250 * <a href="https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a> 251 * and return a value. 252 * @param <V> the result type of method <code>call</code> 253 * @param callable The callable to execute 254 * @return The computed result 255 * @since 7204 256 */ 257 public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) { 258 if (SwingUtilities.isEventDispatchThread()) { 259 try { 260 return callable.call(); 261 } catch (Exception e) { // NOPMD 262 handleEDTException(e); 263 return null; 264 } 265 } else { 266 FutureTask<V> task = new FutureTask<>(callable); 267 SwingUtilities.invokeLater(task); 268 try { 269 return task.get(); 270 } catch (InterruptedException | ExecutionException e) { 271 handleEDTException(e); 272 return null; 273 } 274 } 275 } 276 277 /** 278 * This function fails if it was not called from the EDT thread. 279 * @throws IllegalStateException if called from wrong thread. 280 * @since 10271 281 */ 282 public static void assertCallFromEdt() { 283 if (!SwingUtilities.isEventDispatchThread()) { 284 throw new IllegalStateException( 285 "Needs to be called from the EDT thread, not from " + Thread.currentThread().getName()); 286 } 287 } 288 289 /** 290 * Warns user about a dangerous action requiring confirmation. 291 * @param title Title of dialog 292 * @param content Content of dialog 293 * @param baseActionIcon Unused? FIXME why is this parameter unused? 294 * @param continueToolTip Tooltip to display for "continue" button 295 * @return true if the user wants to cancel, false if they want to continue 296 */ 297 public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) { 298 ExtendedDialog dlg = new ExtendedDialog(MainApplication.getMainFrame(), 299 title, tr("Cancel"), tr("Continue")); 300 dlg.setContent(content); 301 dlg.setButtonIcons( 302 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(), 303 new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( 304 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()); 305 dlg.setToolTipTexts(tr("Cancel"), continueToolTip); 306 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 307 dlg.setCancelButton(1); 308 return dlg.showDialog().getValue() != 2; 309 } 310 311 /** 312 * Notifies user about an error received from an external source as an HTML page. 313 * @param parent Parent component 314 * @param title Title of dialog 315 * @param message Message displayed at the top of the dialog 316 * @param html HTML content to display (real error message) 317 * @since 7312 318 */ 319 public static void notifyUserHtmlError(Component parent, String title, String message, String html) { 320 JPanel p = new JPanel(new GridBagLayout()); 321 p.add(new JLabel(message), GBC.eol()); 322 p.add(new JLabel(tr("Received error page:")), GBC.eol()); 323 JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html)); 324 sp.setPreferredSize(new Dimension(640, 240)); 325 p.add(sp, GBC.eol().fill(GBC.BOTH)); 326 327 ExtendedDialog ed = new ExtendedDialog(parent, title, tr("OK")); 328 ed.setButtonIcons("ok"); 329 ed.setContent(p); 330 ed.showDialog(); 331 } 332 333 /** 334 * Replies the disabled (grayed) version of the specified image. 335 * @param image The image to disable 336 * @return The disabled (grayed) version of the specified image, brightened by 20%. 337 * @since 5484 338 */ 339 public static Image getDisabledImage(Image image) { 340 return Toolkit.getDefaultToolkit().createImage( 341 new FilteredImageSource(image.getSource(), new GrayFilter(true, 20))); 342 } 343 344 /** 345 * Replies the disabled (grayed) version of the specified icon. 346 * @param icon The icon to disable 347 * @return The disabled (grayed) version of the specified icon, brightened by 20%. 348 * @since 5484 349 */ 350 public static ImageIcon getDisabledIcon(ImageIcon icon) { 351 return new ImageIcon(getDisabledImage(icon.getImage())); 352 } 353 354 /** 355 * Attaches a {@code HierarchyListener} to the specified {@code Component} that 356 * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog 357 * to make it resizeable. 358 * @param pane The component that will be displayed 359 * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null 360 * @return {@code pane} 361 * @since 5493 362 */ 363 public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) { 364 if (pane != null) { 365 pane.addHierarchyListener(e -> { 366 Window window = SwingUtilities.getWindowAncestor(pane); 367 if (window instanceof Dialog) { 368 Dialog dialog = (Dialog) window; 369 if (!dialog.isResizable()) { 370 dialog.setResizable(true); 371 if (minDimension != null) { 372 dialog.setMinimumSize(minDimension); 373 } 374 } 375 } 376 }); 377 } 378 return pane; 379 } 380 381 /** 382 * Schedules a new Timer to be run in the future (once or several times). 383 * @param initialDelay milliseconds for the initial and between-event delay if repeatable 384 * @param actionListener an initial listener; can be null 385 * @param repeats specify false to make the timer stop after sending its first action event 386 * @return The (started) timer. 387 * @since 5735 388 */ 389 public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) { 390 Timer timer = new Timer(initialDelay, actionListener); 391 timer.setRepeats(repeats); 392 timer.start(); 393 return timer; 394 } 395 396 /** 397 * Return s new BasicStroke object with given thickness and style 398 * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty 399 * @return stroke for drawing 400 * @see StrokeProperty 401 */ 402 public static Stroke getCustomizedStroke(String code) { 403 return StrokeProperty.getFromString(code); 404 } 405 406 /** 407 * Gets the font used to display monospaced text in a component, if possible. 408 * @param component The component 409 * @return the font used to display monospaced text in a component, if possible 410 * @since 7896 411 */ 412 public static Font getMonospacedFont(JComponent component) { 413 // Special font for Khmer script 414 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 415 return component.getFont(); 416 } else { 417 return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize()); 418 } 419 } 420 421 /** 422 * Gets the font used to display JOSM title in about dialog and splash screen. 423 * @return title font 424 * @since 5797 425 */ 426 public static Font getTitleFont() { 427 return new Font("SansSerif", Font.BOLD, 23); 428 } 429 430 /** 431 * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}. 432 * @param panel The component to embed 433 * @return the vertical scrollable {@code JScrollPane} 434 * @since 6666 435 */ 436 public static JScrollPane embedInVerticalScrollPane(Component panel) { 437 return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 438 } 439 440 /** 441 * Set the default unit increment for a {@code JScrollPane}. 442 * 443 * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane} 444 * is a {@code JPanel} or other component that does not implement the {@link Scrollable} 445 * interface. 446 * The default unit increment is 1 pixel. Multiplied by the number of unit increments 447 * per mouse wheel "click" (platform dependent, usually 3), this makes a very 448 * sluggish mouse wheel experience. 449 * This methods sets the unit increment to a larger, more reasonable value. 450 * @param sp the scroll pane 451 * @return the scroll pane (same object) with fixed unit increment 452 * @throws IllegalArgumentException if the component inside of the scroll pane 453 * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer}, 454 * {@code JList}, {@code JTextComponent} and {@code JTable}) 455 */ 456 public static JScrollPane setDefaultIncrement(JScrollPane sp) { 457 if (sp.getViewport().getView() instanceof Scrollable) { 458 throw new IllegalArgumentException(); 459 } 460 sp.getVerticalScrollBar().setUnitIncrement(10); 461 sp.getHorizontalScrollBar().setUnitIncrement(10); 462 return sp; 463 } 464 465 /** 466 * Sets a global font for all UI, replacing default font of current look and feel. 467 * @param name Font name. It is up to the caller to make sure the font exists 468 * @throws IllegalArgumentException if name is null 469 * @since 7896 470 */ 471 public static void setUIFont(String name) { 472 CheckParameterUtil.ensureParameterNotNull(name, "name"); 473 Logging.info("Setting "+name+" as the default UI font"); 474 Enumeration<?> keys = UIManager.getDefaults().keys(); 475 while (keys.hasMoreElements()) { 476 Object key = keys.nextElement(); 477 Object value = UIManager.get(key); 478 if (value instanceof FontUIResource) { 479 FontUIResource fui = (FontUIResource) value; 480 UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize())); 481 } 482 } 483 } 484 485 /** 486 * Sets the background color for this component, and adjust the foreground color so the text remains readable. 487 * @param c component 488 * @param background background color 489 * @since 9223 490 */ 491 public static void setBackgroundReadable(JComponent c, Color background) { 492 c.setBackground(background); 493 c.setForeground(ColorHelper.getForegroundColor(background)); 494 } 495 496 /** 497 * Gets the size of the screen. On systems with multiple displays, the primary display is used. 498 * This method returns always 800x600 in headless mode (useful for unit tests). 499 * @return the size of this toolkit's screen, in pixels, or 800x600 500 * @see Toolkit#getScreenSize 501 * @since 9576 502 */ 503 public static Dimension getScreenSize() { 504 return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize(); 505 } 506 507 /** 508 * Gets the size of the screen. On systems with multiple displays, 509 * contrary to {@link #getScreenSize()}, the biggest display is used. 510 * This method returns always 800x600 in headless mode (useful for unit tests). 511 * @return the size of maximum screen, in pixels, or 800x600 512 * @see Toolkit#getScreenSize 513 * @since 10470 514 */ 515 public static Dimension getMaximumScreenSize() { 516 if (GraphicsEnvironment.isHeadless()) { 517 return new Dimension(800, 600); 518 } 519 520 int height = 0; 521 int width = 0; 522 for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { 523 DisplayMode dm = gd.getDisplayMode(); 524 height = Math.max(height, dm.getHeight()); 525 width = Math.max(width, dm.getWidth()); 526 } 527 if (height == 0 || width == 0) { 528 return new Dimension(800, 600); 529 } 530 return new Dimension(width, height); 531 } 532 533 /** 534 * Returns the first <code>Window</code> ancestor of event source, or 535 * {@code null} if event source is not a component contained inside a <code>Window</code>. 536 * @param e event object 537 * @return a Window, or {@code null} 538 * @since 9916 539 */ 540 public static Window getWindowAncestorFor(EventObject e) { 541 if (e != null) { 542 Object source = e.getSource(); 543 if (source instanceof Component) { 544 Window ancestor = SwingUtilities.getWindowAncestor((Component) source); 545 if (ancestor != null) { 546 return ancestor; 547 } else { 548 Container parent = ((Component) source).getParent(); 549 if (parent instanceof JPopupMenu) { 550 Component invoker = ((JPopupMenu) parent).getInvoker(); 551 return SwingUtilities.getWindowAncestor(invoker); 552 } 553 } 554 } 555 } 556 return null; 557 } 558 559 /** 560 * Extends tooltip dismiss delay to a default value of 1 minute for the given component. 561 * @param c component 562 * @since 10024 563 */ 564 public static void extendTooltipDelay(Component c) { 565 extendTooltipDelay(c, 60_000); 566 } 567 568 /** 569 * Extends tooltip dismiss delay to the specified value for the given component. 570 * @param c component 571 * @param delay tooltip dismiss delay in milliseconds 572 * @see <a href="http://stackoverflow.com/a/6517902/2257172">http://stackoverflow.com/a/6517902/2257172</a> 573 * @since 10024 574 */ 575 public static void extendTooltipDelay(Component c, final int delay) { 576 final int defaultDismissTimeout = ToolTipManager.sharedInstance().getDismissDelay(); 577 c.addMouseListener(new MouseAdapter() { 578 @Override 579 public void mouseEntered(MouseEvent me) { 580 ToolTipManager.sharedInstance().setDismissDelay(delay); 581 } 582 583 @Override 584 public void mouseExited(MouseEvent me) { 585 ToolTipManager.sharedInstance().setDismissDelay(defaultDismissTimeout); 586 } 587 }); 588 } 589 590 /** 591 * Returns the specified component's <code>Frame</code> without throwing exception in headless mode. 592 * 593 * @param parentComponent the <code>Component</code> to check for a <code>Frame</code> 594 * @return the <code>Frame</code> that contains the component, or <code>getRootFrame</code> 595 * if the component is <code>null</code>, or does not have a valid <code>Frame</code> parent 596 * @see JOptionPane#getFrameForComponent 597 * @see GraphicsEnvironment#isHeadless 598 * @since 10035 599 */ 600 public static Frame getFrameForComponent(Component parentComponent) { 601 try { 602 return JOptionPane.getFrameForComponent(parentComponent); 603 } catch (HeadlessException e) { 604 Logging.debug(e); 605 return null; 606 } 607 } 608 609 /** 610 * Localizations for file chooser dialog. 611 * For some locales (e.g. de, fr) translations are provided 612 * by Java, but not for others (e.g. ru, uk). 613 * @since 12644 (moved from I18n) 614 */ 615 public static void translateJavaInternalMessages() { 616 Locale l = Locale.getDefault(); 617 618 AbstractFileChooser.setDefaultLocale(l); 619 JFileChooser.setDefaultLocale(l); 620 JColorChooser.setDefaultLocale(l); 621 for (String key : JAVA_INTERNAL_MESSAGE_KEYS) { 622 String us = UIManager.getString(key, Locale.US); 623 String loc = UIManager.getString(key, l); 624 // only provide custom translation if it is not already localized by Java 625 if (us != null && us.equals(loc)) { 626 UIManager.put(key, tr(us)); 627 } 628 } 629 } 630 631 /** 632 * Setup special font for Khmer script, as the default Java fonts do not display these characters. 633 * @since 12644 (moved from I18n) 634 * @since 8282 635 */ 636 public static void setupLanguageFonts() { 637 // Use special font for Khmer script, as the default Java font do not display these characters 638 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 639 Collection<String> fonts = Arrays.asList( 640 GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); 641 for (String f : new String[]{"Khmer UI", "DaunPenh", "MoolBoran"}) { 642 if (fonts.contains(f)) { 643 setUIFont(f); 644 break; 645 } 646 } 647 } 648 } 649 650 /** 651 * Destroys recursively all {@link Destroyable} components of a given container, and optionnally the container itself. 652 * @param component the component to destroy 653 * @param destroyItself whether to destroy the component itself 654 * @since 14463 655 */ 656 public static void destroyComponents(Component component, boolean destroyItself) { 657 if (component instanceof Container) { 658 for (Component c: ((Container) component).getComponents()) { 659 destroyComponents(c, true); 660 } 661 } 662 if (destroyItself && component instanceof Destroyable) { 663 ((Destroyable) component).destroy(); 664 } 665 } 666}