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