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.BasicStroke; 007import java.awt.Color; 008import java.awt.Component; 009import java.awt.Container; 010import java.awt.Dialog; 011import java.awt.Dimension; 012import java.awt.DisplayMode; 013import java.awt.Font; 014import java.awt.GraphicsDevice; 015import java.awt.GraphicsEnvironment; 016import java.awt.GridBagLayout; 017import java.awt.Image; 018import java.awt.Stroke; 019import java.awt.Toolkit; 020import java.awt.Window; 021import java.awt.datatransfer.Clipboard; 022import java.awt.event.ActionListener; 023import java.awt.event.HierarchyEvent; 024import java.awt.event.HierarchyListener; 025import java.awt.event.KeyEvent; 026import java.awt.image.FilteredImageSource; 027import java.lang.reflect.InvocationTargetException; 028import java.util.Enumeration; 029import java.util.EventObject; 030import java.util.concurrent.Callable; 031import java.util.concurrent.ExecutionException; 032import java.util.concurrent.FutureTask; 033 034import javax.swing.GrayFilter; 035import javax.swing.Icon; 036import javax.swing.ImageIcon; 037import javax.swing.JComponent; 038import javax.swing.JLabel; 039import javax.swing.JOptionPane; 040import javax.swing.JPanel; 041import javax.swing.JScrollPane; 042import javax.swing.Scrollable; 043import javax.swing.SwingUtilities; 044import javax.swing.Timer; 045import javax.swing.UIManager; 046import javax.swing.plaf.FontUIResource; 047 048import org.openstreetmap.josm.Main; 049import org.openstreetmap.josm.gui.ExtendedDialog; 050import org.openstreetmap.josm.gui.widgets.HtmlPanel; 051import org.openstreetmap.josm.tools.CheckParameterUtil; 052import org.openstreetmap.josm.tools.ColorHelper; 053import org.openstreetmap.josm.tools.GBC; 054import org.openstreetmap.josm.tools.ImageOverlay; 055import org.openstreetmap.josm.tools.ImageProvider; 056import org.openstreetmap.josm.tools.ImageProvider.ImageSizes; 057import org.openstreetmap.josm.tools.LanguageInfo; 058 059/** 060 * basic gui utils 061 */ 062public final class GuiHelper { 063 064 private GuiHelper() { 065 // Hide default constructor for utils classes 066 } 067 068 /** 069 * disable / enable a component and all its child components 070 * @param root component 071 * @param enabled enabled state 072 */ 073 public static void setEnabledRec(Container root, boolean enabled) { 074 root.setEnabled(enabled); 075 Component[] children = root.getComponents(); 076 for (Component child : children) { 077 if (child instanceof Container) { 078 setEnabledRec((Container) child, enabled); 079 } else { 080 child.setEnabled(enabled); 081 } 082 } 083 } 084 085 public static void executeByMainWorkerInEDT(final Runnable task) { 086 Main.worker.submit(new Runnable() { 087 @Override 088 public void run() { 089 runInEDTAndWait(task); 090 } 091 }); 092 } 093 094 /** 095 * Executes asynchronously a runnable in 096 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 097 * @param task The runnable to execute 098 * @see SwingUtilities#invokeLater 099 */ 100 public static void runInEDT(Runnable task) { 101 if (SwingUtilities.isEventDispatchThread()) { 102 task.run(); 103 } else { 104 SwingUtilities.invokeLater(task); 105 } 106 } 107 108 /** 109 * Executes synchronously a runnable in 110 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>. 111 * @param task The runnable to execute 112 * @see SwingUtilities#invokeAndWait 113 */ 114 public static void runInEDTAndWait(Runnable task) { 115 if (SwingUtilities.isEventDispatchThread()) { 116 task.run(); 117 } else { 118 try { 119 SwingUtilities.invokeAndWait(task); 120 } catch (InterruptedException | InvocationTargetException e) { 121 Main.error(e); 122 } 123 } 124 } 125 126 /** 127 * Executes synchronously a callable in 128 * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a> 129 * and return a value. 130 * @param <V> the result type of method <tt>call</tt> 131 * @param callable The callable to execute 132 * @return The computed result 133 * @since 7204 134 */ 135 public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) { 136 if (SwingUtilities.isEventDispatchThread()) { 137 try { 138 return callable.call(); 139 } catch (Exception e) { 140 Main.error(e); 141 return null; 142 } 143 } else { 144 FutureTask<V> task = new FutureTask<>(callable); 145 SwingUtilities.invokeLater(task); 146 try { 147 return task.get(); 148 } catch (InterruptedException | ExecutionException e) { 149 Main.error(e); 150 return null; 151 } 152 } 153 } 154 155 /** 156 * Warns user about a dangerous action requiring confirmation. 157 * @param title Title of dialog 158 * @param content Content of dialog 159 * @param baseActionIcon Unused? FIXME why is this parameter unused? 160 * @param continueToolTip Tooltip to display for "continue" button 161 * @return true if the user wants to cancel, false if they want to continue 162 */ 163 public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) { 164 ExtendedDialog dlg = new ExtendedDialog(Main.parent, 165 title, new String[] {tr("Cancel"), tr("Continue")}); 166 dlg.setContent(content); 167 dlg.setButtonIcons(new Icon[] { 168 new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(), 169 new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay( 170 new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()}); 171 dlg.setToolTipTexts(new String[] { 172 tr("Cancel"), 173 continueToolTip}); 174 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 175 dlg.setCancelButton(1); 176 return dlg.showDialog().getValue() != 2; 177 } 178 179 /** 180 * Notifies user about an error received from an external source as an HTML page. 181 * @param parent Parent component 182 * @param title Title of dialog 183 * @param message Message displayed at the top of the dialog 184 * @param html HTML content to display (real error message) 185 * @since 7312 186 */ 187 public static void notifyUserHtmlError(Component parent, String title, String message, String html) { 188 JPanel p = new JPanel(new GridBagLayout()); 189 p.add(new JLabel(message), GBC.eol()); 190 p.add(new JLabel(tr("Received error page:")), GBC.eol()); 191 JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html)); 192 sp.setPreferredSize(new Dimension(640, 240)); 193 p.add(sp, GBC.eol().fill(GBC.BOTH)); 194 195 ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")}); 196 ed.setButtonIcons(new String[] {"ok.png"}); 197 ed.setContent(p); 198 ed.showDialog(); 199 } 200 201 /** 202 * Replies the disabled (grayed) version of the specified image. 203 * @param image The image to disable 204 * @return The disabled (grayed) version of the specified image, brightened by 20%. 205 * @since 5484 206 */ 207 public static Image getDisabledImage(Image image) { 208 return Toolkit.getDefaultToolkit().createImage( 209 new FilteredImageSource(image.getSource(), new GrayFilter(true, 20))); 210 } 211 212 /** 213 * Replies the disabled (grayed) version of the specified icon. 214 * @param icon The icon to disable 215 * @return The disabled (grayed) version of the specified icon, brightened by 20%. 216 * @since 5484 217 */ 218 public static ImageIcon getDisabledIcon(ImageIcon icon) { 219 return new ImageIcon(getDisabledImage(icon.getImage())); 220 } 221 222 /** 223 * Attaches a {@code HierarchyListener} to the specified {@code Component} that 224 * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog 225 * to make it resizeable. 226 * @param pane The component that will be displayed 227 * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null 228 * @return {@code pane} 229 * @since 5493 230 */ 231 public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) { 232 if (pane != null) { 233 pane.addHierarchyListener(new HierarchyListener() { 234 @Override 235 public void hierarchyChanged(HierarchyEvent e) { 236 Window window = SwingUtilities.getWindowAncestor(pane); 237 if (window instanceof Dialog) { 238 Dialog dialog = (Dialog) window; 239 if (!dialog.isResizable()) { 240 dialog.setResizable(true); 241 if (minDimension != null) { 242 dialog.setMinimumSize(minDimension); 243 } 244 } 245 } 246 } 247 }); 248 } 249 return pane; 250 } 251 252 /** 253 * Schedules a new Timer to be run in the future (once or several times). 254 * @param initialDelay milliseconds for the initial and between-event delay if repeatable 255 * @param actionListener an initial listener; can be null 256 * @param repeats specify false to make the timer stop after sending its first action event 257 * @return The (started) timer. 258 * @since 5735 259 */ 260 public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) { 261 Timer timer = new Timer(initialDelay, actionListener); 262 timer.setRepeats(repeats); 263 timer.start(); 264 return timer; 265 } 266 267 /** 268 * Return s new BasicStroke object with given thickness and style 269 * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty 270 * @return stroke for drawing 271 */ 272 public static Stroke getCustomizedStroke(String code) { 273 String[] s = code.trim().split("[^\\.0-9]+"); 274 275 if (s.length == 0) return new BasicStroke(); 276 float w; 277 try { 278 w = Float.parseFloat(s[0]); 279 } catch (NumberFormatException ex) { 280 w = 1.0f; 281 } 282 if (s.length > 1) { 283 float[] dash = new float[s.length-1]; 284 float sumAbs = 0; 285 try { 286 for (int i = 0; i < s.length-1; i++) { 287 dash[i] = Float.parseFloat(s[i+1]); 288 sumAbs += Math.abs(dash[i]); 289 } 290 } catch (NumberFormatException ex) { 291 Main.error("Error in stroke preference format: "+code); 292 dash = new float[]{5.0f}; 293 } 294 if (sumAbs < 1e-1) { 295 Main.error("Error in stroke dash fomat (all zeros): "+code); 296 return new BasicStroke(w); 297 } 298 // dashed stroke 299 return new BasicStroke(w, BasicStroke.CAP_BUTT, 300 BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f); 301 } else { 302 if (w > 1) { 303 // thick stroke 304 return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 305 } else { 306 // thin stroke 307 return new BasicStroke(w); 308 } 309 } 310 } 311 312 /** 313 * Gets the font used to display monospaced text in a component, if possible. 314 * @param component The component 315 * @return the font used to display monospaced text in a component, if possible 316 * @since 7896 317 */ 318 public static Font getMonospacedFont(JComponent component) { 319 // Special font for Khmer script 320 if ("km".equals(LanguageInfo.getJOSMLocaleCode())) { 321 return component.getFont(); 322 } else { 323 return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize()); 324 } 325 } 326 327 /** 328 * Gets the font used to display JOSM title in about dialog and splash screen. 329 * @return title font 330 * @since 5797 331 */ 332 public static Font getTitleFont() { 333 return new Font("SansSerif", Font.BOLD, 23); 334 } 335 336 /** 337 * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}. 338 * @param panel The component to embed 339 * @return the vertical scrollable {@code JScrollPane} 340 * @since 6666 341 */ 342 public static JScrollPane embedInVerticalScrollPane(Component panel) { 343 return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 344 } 345 346 /** 347 * Set the default unit increment for a {@code JScrollPane}. 348 * 349 * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane} 350 * is a {@code JPanel} or other component that does not implement the {@link Scrollable} 351 * interface. 352 * The default unit increment is 1 pixel. Multiplied by the number of unit increments 353 * per mouse wheel "click" (platform dependent, usually 3), this makes a very 354 * sluggish mouse wheel experience. 355 * This methods sets the unit increment to a larger, more reasonable value. 356 * @param sp the scroll pane 357 * @return the scroll pane (same object) with fixed unit increment 358 * @throws IllegalArgumentException if the component inside of the scroll pane 359 * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer}, 360 * {@code JList}, {@code JTextComponent} and {@code JTable}) 361 */ 362 public static JScrollPane setDefaultIncrement(JScrollPane sp) { 363 if (sp.getViewport().getView() instanceof Scrollable) { 364 throw new IllegalArgumentException(); 365 } 366 sp.getVerticalScrollBar().setUnitIncrement(10); 367 sp.getHorizontalScrollBar().setUnitIncrement(10); 368 return sp; 369 } 370 371 /** 372 * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts. 373 * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but: 374 * <ul> 375 * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended 376 * modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li> 377 * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li> 378 * </ul> 379 * @return extended modifier key used as the appropriate accelerator key for menu shortcuts 380 * @since 7539 381 */ 382 public static int getMenuShortcutKeyMaskEx() { 383 return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; 384 } 385 386 /** 387 * Sets a global font for all UI, replacing default font of current look and feel. 388 * @param name Font name. It is up to the caller to make sure the font exists 389 * @throws IllegalArgumentException if name is null 390 * @since 7896 391 */ 392 public static void setUIFont(String name) { 393 CheckParameterUtil.ensureParameterNotNull(name, "name"); 394 Main.info("Setting "+name+" as the default UI font"); 395 Enumeration<?> keys = UIManager.getDefaults().keys(); 396 while (keys.hasMoreElements()) { 397 Object key = keys.nextElement(); 398 Object value = UIManager.get(key); 399 if (value instanceof FontUIResource) { 400 FontUIResource fui = (FontUIResource) value; 401 UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize())); 402 } 403 } 404 } 405 406 /** 407 * Sets the background color for this component, and adjust the foreground color so the text remains readable. 408 * @param c component 409 * @param background background color 410 * @since 9223 411 */ 412 public static void setBackgroundReadable(JComponent c, Color background) { 413 c.setBackground(background); 414 c.setForeground(ColorHelper.getForegroundColor(background)); 415 } 416 417 /** 418 * Gets the size of the screen. On systems with multiple displays, the primary display is used. 419 * This method returns always 800x600 in headless mode (useful for unit tests). 420 * @return the size of this toolkit's screen, in pixels, or 800x600 421 * @see Toolkit#getScreenSize 422 * @since 9576 423 */ 424 public static Dimension getScreenSize() { 425 return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize(); 426 } 427 428 /** 429 * Gets the size of the screen. On systems with multiple displays, 430 * contrary to {@link #getScreenSize()}, the biggest display is used. 431 * This method returns always 800x600 in headless mode (useful for unit tests). 432 * @return the size of maximum screen, in pixels, or 800x600 433 * @see Toolkit#getScreenSize 434 * @since 9576 435 */ 436 437 public static Dimension getMaxiumScreenSize() { 438 if (GraphicsEnvironment.isHeadless()) { 439 return new Dimension(800, 600); 440 } 441 442 int height = 0; 443 int width = 0; 444 for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { 445 DisplayMode dm = gd.getDisplayMode(); 446 height = Math.max(height, dm.getHeight()); 447 width = Math.max(width, dm.getWidth()); 448 } 449 if (height == 0 || width == 0) { 450 return new Dimension(800, 600); 451 } 452 return new Dimension(width, height); 453 } 454 455 /** 456 * Gets the singleton instance of the system selection as a <code>Clipboard</code> object. 457 * This allows an application to read and modify the current, system-wide selection. 458 * @return the system selection as a <code>Clipboard</code>, or <code>null</code> if the native platform does not 459 * support a system selection <code>Clipboard</code> or if GraphicsEnvironment.isHeadless() returns true 460 * @see Toolkit#getSystemSelection 461 * @since 9576 462 */ 463 public static Clipboard getSystemSelection() { 464 return GraphicsEnvironment.isHeadless() ? null : Toolkit.getDefaultToolkit().getSystemSelection(); 465 } 466 467 /** 468 * Returns the first <code>Window</code> ancestor of event source, or 469 * {@code null} if event source is not a component contained inside a <code>Window</code>. 470 * @param e event object 471 * @return a Window, or {@code null} 472 * @since 9916 473 */ 474 public static Window getWindowAncestorFor(EventObject e) { 475 return e != null && e.getSource() instanceof Component ? SwingUtilities.getWindowAncestor((Component) e.getSource()) : null; 476 } 477}