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