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