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