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     * 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 -&gt; thickness=3.5px; 3.5 10 5 -&gt; 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}