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.GraphicsEnvironment; 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.Arrays; 025import java.util.Enumeration; 026import java.util.List; 027import java.util.concurrent.Callable; 028import java.util.concurrent.ExecutionException; 029import java.util.concurrent.FutureTask; 030 031import javax.swing.GrayFilter; 032import javax.swing.Icon; 033import javax.swing.ImageIcon; 034import javax.swing.JComponent; 035import javax.swing.JLabel; 036import javax.swing.JOptionPane; 037import javax.swing.JPanel; 038import javax.swing.JScrollPane; 039import javax.swing.SwingUtilities; 040import javax.swing.Timer; 041import javax.swing.UIManager; 042import javax.swing.plaf.FontUIResource; 043 044import org.openstreetmap.josm.Main; 045import org.openstreetmap.josm.gui.ExtendedDialog; 046import org.openstreetmap.josm.gui.widgets.HtmlPanel; 047import org.openstreetmap.josm.tools.CheckParameterUtil; 048import org.openstreetmap.josm.tools.GBC; 049import org.openstreetmap.josm.tools.ImageProvider; 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 final 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 ImageProvider.get("cancel"), 158 ImageProvider.overlay( 159 ImageProvider.get("upload"), 160 new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)), 161 ImageProvider.OverlayPosition.SOUTHEAST)}); 162 dlg.setToolTipTexts(new String[] { 163 tr("Cancel"), 164 continueToolTip}); 165 dlg.setIcon(JOptionPane.WARNING_MESSAGE); 166 dlg.setCancelButton(1); 167 return dlg.showDialog().getValue() != 2; 168 } 169 170 /** 171 * Notifies user about an error received from an external source as an HTML page. 172 * @param parent Parent component 173 * @param title Title of dialog 174 * @param message Message displayed at the top of the dialog 175 * @param html HTML content to display (real error message) 176 * @since 7312 177 */ 178 public static final void notifyUserHtmlError(Component parent, String title, String message, String html) { 179 JPanel p = new JPanel(new GridBagLayout()); 180 p.add(new JLabel(message), GBC.eol()); 181 p.add(new JLabel(tr("Received error page:")), GBC.eol()); 182 JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html)); 183 sp.setPreferredSize(new Dimension(640, 240)); 184 p.add(sp, GBC.eol().fill(GBC.BOTH)); 185 186 ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")}); 187 ed.setButtonIcons(new String[] {"ok.png"}); 188 ed.setContent(p); 189 ed.showDialog(); 190 } 191 192 /** 193 * Replies the disabled (grayed) version of the specified image. 194 * @param image The image to disable 195 * @return The disabled (grayed) version of the specified image, brightened by 20%. 196 * @since 5484 197 */ 198 public static final Image getDisabledImage(Image image) { 199 return Toolkit.getDefaultToolkit().createImage( 200 new FilteredImageSource(image.getSource(), new GrayFilter(true, 20))); 201 } 202 203 /** 204 * Replies the disabled (grayed) version of the specified icon. 205 * @param icon The icon to disable 206 * @return The disabled (grayed) version of the specified icon, brightened by 20%. 207 * @since 5484 208 */ 209 public static final ImageIcon getDisabledIcon(ImageIcon icon) { 210 return new ImageIcon(getDisabledImage(icon.getImage())); 211 } 212 213 /** 214 * Attaches a {@code HierarchyListener} to the specified {@code Component} that 215 * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog 216 * to make it resizeable. 217 * @param pane The component that will be displayed 218 * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null 219 * @return {@code pane} 220 * @since 5493 221 */ 222 public static final Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) { 223 if (pane != null) { 224 pane.addHierarchyListener(new HierarchyListener() { 225 @Override 226 public void hierarchyChanged(HierarchyEvent e) { 227 Window window = SwingUtilities.getWindowAncestor(pane); 228 if (window instanceof Dialog) { 229 Dialog dialog = (Dialog)window; 230 if (!dialog.isResizable()) { 231 dialog.setResizable(true); 232 if (minDimension != null) { 233 dialog.setMinimumSize(minDimension); 234 } 235 } 236 } 237 } 238 }); 239 } 240 return pane; 241 } 242 243 /** 244 * Schedules a new Timer to be run in the future (once or several times). 245 * @param initialDelay milliseconds for the initial and between-event delay if repeatable 246 * @param actionListener an initial listener; can be null 247 * @param repeats specify false to make the timer stop after sending its first action event 248 * @return The (started) timer. 249 * @since 5735 250 */ 251 public static final Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) { 252 Timer timer = new Timer(initialDelay, actionListener); 253 timer.setRepeats(repeats); 254 timer.start(); 255 return timer; 256 } 257 258 /** 259 * Return s new BasicStroke object with given thickness and style 260 * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty 261 * @return stroke for drawing 262 */ 263 public static Stroke getCustomizedStroke(String code) { 264 String[] s = code.trim().split("[^\\.0-9]+"); 265 266 if (s.length==0) return new BasicStroke(); 267 float w; 268 try { 269 w = Float.parseFloat(s[0]); 270 } catch (NumberFormatException ex) { 271 w = 1.0f; 272 } 273 if (s.length>1) { 274 float[] dash= new float[s.length-1]; 275 float sumAbs = 0; 276 try { 277 for (int i=0; i<s.length-1; i++) { 278 dash[i] = Float.parseFloat(s[i+1]); 279 sumAbs += Math.abs(dash[i]); 280 } 281 } catch (NumberFormatException ex) { 282 Main.error("Error in stroke preference format: "+code); 283 dash = new float[]{5.0f}; 284 } 285 if (sumAbs < 1e-1) { 286 Main.error("Error in stroke dash fomat (all zeros): "+code); 287 return new BasicStroke(w); 288 } 289 // dashed stroke 290 return new BasicStroke(w, BasicStroke.CAP_BUTT, 291 BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f); 292 } else { 293 if (w>1) { 294 // thick stroke 295 return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); 296 } else { 297 // thin stroke 298 return new BasicStroke(w); 299 } 300 } 301 } 302 303 /** 304 * Gets the font used to display monospaced text in a component, if possible. 305 * @param component The component 306 * @return the font used to display monospaced text in a component, if possible 307 * @since 7896 308 */ 309 public static Font getMonospacedFont(JComponent component) { 310 // Special font for Khmer script 311 if ("km".equals(Main.pref.get("language"))) { 312 return component.getFont(); 313 } else { 314 return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize()); 315 } 316 } 317 318 /** 319 * Gets the font used to display JOSM title in about dialog and splash screen. 320 * @return By order or priority, the first font available in local fonts: 321 * 1. Helvetica Bold 20 322 * 2. Calibri Bold 23 323 * 3. Arial Bold 20 324 * 4. SansSerif Bold 20 325 * Except if current language is Khmer, where it will be current font at size 20 326 * @since 5797 327 */ 328 public static Font getTitleFont() { 329 List<String> fonts = Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()); 330 // Special font for Khmer script 331 if ("km".equals(Main.pref.get("language"))) { 332 return UIManager.getFont("Label.font").deriveFont(20.0f); 333 } 334 // Helvetica is the preferred choice but is not available by default on Windows 335 // (https://www.microsoft.com/typography/fonts/product.aspx?pid=161) 336 if (fonts.contains("Helvetica")) { 337 return new Font("Helvetica", Font.BOLD, 20); 338 // Calibri is the default Windows font since Windows Vista but is not available on older versions of Windows, where Arial is preferred 339 } else if (fonts.contains("Calibri")) { 340 return new Font("Calibri", Font.BOLD, 23); 341 } else if (fonts.contains("Arial")) { 342 return new Font("Arial", Font.BOLD, 20); 343 // No luck, nothing found, fallback to one of the 5 fonts provided with Java (Serif, SansSerif, Monospaced, Dialog, and DialogInput) 344 } else { 345 return new Font("SansSerif", Font.BOLD, 20); 346 } 347 } 348 349 /** 350 * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}. 351 * @param panel The component to embed 352 * @return the vertical scrollable {@code JScrollPane} 353 * @since 6666 354 */ 355 public static JScrollPane embedInVerticalScrollPane(Component panel) { 356 return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); 357 } 358 359 /** 360 * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts. 361 * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but: 362 * <ul> 363 * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended 364 * modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li> 365 * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li> 366 * </ul> 367 * @return extended modifier key used as the appropriate accelerator key for menu shortcuts 368 * @since 7539 369 */ 370 public static int getMenuShortcutKeyMaskEx() { 371 return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK; 372 } 373 374 /** 375 * Sets a global font for all UI, replacing default font of current look and feel. 376 * @param name Font name. It is up to the caller to make sure the font exists 377 * @since 7896 378 * @throws IllegalArgumentException if name is null 379 */ 380 public static void setUIFont(String name) { 381 CheckParameterUtil.ensureParameterNotNull(name, "name"); 382 Main.info("Setting "+name+" as the default UI font"); 383 Enumeration<?> keys = UIManager.getDefaults().keys(); 384 while (keys.hasMoreElements()) { 385 Object key = keys.nextElement(); 386 Object value = UIManager.get(key); 387 if (value != null && value instanceof FontUIResource) { 388 FontUIResource fui = (FontUIResource)value; 389 UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize())); 390 } 391 } 392 } 393}