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 -&gt; thickness=3.5px; 3.5 10 5 -&gt; 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}