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