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