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