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