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