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