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