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.Enumeration;
026import java.util.List;
027import java.util.concurrent.Callable;
028import java.util.concurrent.ExecutionException;
029import java.util.concurrent.FutureTask;
030
031import javax.swing.GrayFilter;
032import javax.swing.Icon;
033import javax.swing.ImageIcon;
034import javax.swing.JComponent;
035import javax.swing.JLabel;
036import javax.swing.JOptionPane;
037import javax.swing.JPanel;
038import javax.swing.JScrollPane;
039import javax.swing.SwingUtilities;
040import javax.swing.Timer;
041import javax.swing.UIManager;
042import javax.swing.plaf.FontUIResource;
043
044import org.openstreetmap.josm.Main;
045import org.openstreetmap.josm.gui.ExtendedDialog;
046import org.openstreetmap.josm.gui.widgets.HtmlPanel;
047import org.openstreetmap.josm.tools.CheckParameterUtil;
048import org.openstreetmap.josm.tools.GBC;
049import org.openstreetmap.josm.tools.ImageProvider;
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 final 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                ImageProvider.get("cancel"),
158                ImageProvider.overlay(
159                        ImageProvider.get("upload"),
160                        new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)),
161                        ImageProvider.OverlayPosition.SOUTHEAST)});
162        dlg.setToolTipTexts(new String[] {
163                tr("Cancel"),
164                continueToolTip});
165        dlg.setIcon(JOptionPane.WARNING_MESSAGE);
166        dlg.setCancelButton(1);
167        return dlg.showDialog().getValue() != 2;
168    }
169
170    /**
171     * Notifies user about an error received from an external source as an HTML page.
172     * @param parent Parent component
173     * @param title Title of dialog
174     * @param message Message displayed at the top of the dialog
175     * @param html HTML content to display (real error message)
176     * @since 7312
177     */
178    public static final void notifyUserHtmlError(Component parent, String title, String message, String html) {
179        JPanel p = new JPanel(new GridBagLayout());
180        p.add(new JLabel(message), GBC.eol());
181        p.add(new JLabel(tr("Received error page:")), GBC.eol());
182        JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html));
183        sp.setPreferredSize(new Dimension(640, 240));
184        p.add(sp, GBC.eol().fill(GBC.BOTH));
185
186        ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")});
187        ed.setButtonIcons(new String[] {"ok.png"});
188        ed.setContent(p);
189        ed.showDialog();
190    }
191
192    /**
193     * Replies the disabled (grayed) version of the specified image.
194     * @param image The image to disable
195     * @return The disabled (grayed) version of the specified image, brightened by 20%.
196     * @since 5484
197     */
198    public static final Image getDisabledImage(Image image) {
199        return Toolkit.getDefaultToolkit().createImage(
200                new FilteredImageSource(image.getSource(), new GrayFilter(true, 20)));
201    }
202
203    /**
204     * Replies the disabled (grayed) version of the specified icon.
205     * @param icon The icon to disable
206     * @return The disabled (grayed) version of the specified icon, brightened by 20%.
207     * @since 5484
208     */
209    public static final ImageIcon getDisabledIcon(ImageIcon icon) {
210        return new ImageIcon(getDisabledImage(icon.getImage()));
211    }
212
213    /**
214     * Attaches a {@code HierarchyListener} to the specified {@code Component} that
215     * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog
216     * to make it resizeable.
217     * @param pane The component that will be displayed
218     * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null
219     * @return {@code pane}
220     * @since 5493
221     */
222    public static final Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) {
223        if (pane != null) {
224            pane.addHierarchyListener(new HierarchyListener() {
225                @Override
226                public void hierarchyChanged(HierarchyEvent e) {
227                    Window window = SwingUtilities.getWindowAncestor(pane);
228                    if (window instanceof Dialog) {
229                        Dialog dialog = (Dialog)window;
230                        if (!dialog.isResizable()) {
231                            dialog.setResizable(true);
232                            if (minDimension != null) {
233                                dialog.setMinimumSize(minDimension);
234                            }
235                        }
236                    }
237                }
238            });
239        }
240        return pane;
241    }
242
243    /**
244     * Schedules a new Timer to be run in the future (once or several times).
245     * @param initialDelay milliseconds for the initial and between-event delay if repeatable
246     * @param actionListener an initial listener; can be null
247     * @param repeats specify false to make the timer stop after sending its first action event
248     * @return The (started) timer.
249     * @since 5735
250     */
251    public static final Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) {
252        Timer timer = new Timer(initialDelay, actionListener);
253        timer.setRepeats(repeats);
254        timer.start();
255        return timer;
256    }
257
258    /**
259     * Return s new BasicStroke object with given thickness and style
260     * @param code = 3.5 -&gt; thickness=3.5px; 3.5 10 5 -&gt; thickness=3.5px, dashed: 10px filled + 5px empty
261     * @return stroke for drawing
262     */
263    public static Stroke getCustomizedStroke(String code) {
264        String[] s = code.trim().split("[^\\.0-9]+");
265
266        if (s.length==0) return new BasicStroke();
267        float w;
268        try {
269            w = Float.parseFloat(s[0]);
270        } catch (NumberFormatException ex) {
271            w = 1.0f;
272        }
273        if (s.length>1) {
274            float[] dash= new float[s.length-1];
275            float sumAbs = 0;
276            try {
277                for (int i=0; i<s.length-1; i++) {
278                   dash[i] = Float.parseFloat(s[i+1]);
279                   sumAbs += Math.abs(dash[i]);
280                }
281            } catch (NumberFormatException ex) {
282                Main.error("Error in stroke preference format: "+code);
283                dash = new float[]{5.0f};
284            }
285            if (sumAbs < 1e-1) {
286                Main.error("Error in stroke dash fomat (all zeros): "+code);
287                return new BasicStroke(w);
288            }
289            // dashed stroke
290            return new BasicStroke(w, BasicStroke.CAP_BUTT,
291                    BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
292        } else {
293            if (w>1) {
294                // thick stroke
295                return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
296            } else {
297                // thin stroke
298                return new BasicStroke(w);
299            }
300        }
301    }
302
303    /**
304     * Gets the font used to display monospaced text in a component, if possible.
305     * @param component The component
306     * @return the font used to display monospaced text in a component, if possible
307     * @since 7896
308     */
309    public static Font getMonospacedFont(JComponent component) {
310        // Special font for Khmer script
311        if ("km".equals(Main.pref.get("language"))) {
312            return component.getFont();
313        } else {
314            return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize());
315        }
316    }
317
318    /**
319     * Gets the font used to display JOSM title in about dialog and splash screen.
320     * @return By order or priority, the first font available in local fonts:
321     *         1. Helvetica Bold 20
322     *         2. Calibri Bold 23
323     *         3. Arial Bold 20
324     *         4. SansSerif Bold 20
325     *         Except if current language is Khmer, where it will be current font at size 20
326     * @since 5797
327     */
328    public static Font getTitleFont() {
329        List<String> fonts = Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
330        // Special font for Khmer script
331        if ("km".equals(Main.pref.get("language"))) {
332            return UIManager.getFont("Label.font").deriveFont(20.0f);
333        }
334        // Helvetica is the preferred choice but is not available by default on Windows
335        // (https://www.microsoft.com/typography/fonts/product.aspx?pid=161)
336        if (fonts.contains("Helvetica")) {
337            return new Font("Helvetica", Font.BOLD, 20);
338        // Calibri is the default Windows font since Windows Vista but is not available on older versions of Windows, where Arial is preferred
339        } else if (fonts.contains("Calibri")) {
340            return new Font("Calibri", Font.BOLD, 23);
341        } else if (fonts.contains("Arial")) {
342            return new Font("Arial", Font.BOLD, 20);
343        // No luck, nothing found, fallback to one of the 5 fonts provided with Java (Serif, SansSerif, Monospaced, Dialog, and DialogInput)
344        } else {
345            return new Font("SansSerif", Font.BOLD, 20);
346        }
347    }
348
349    /**
350     * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}.
351     * @param panel The component to embed
352     * @return the vertical scrollable {@code JScrollPane}
353     * @since 6666
354     */
355    public static JScrollPane embedInVerticalScrollPane(Component panel) {
356        return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
357    }
358
359    /**
360     * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts.
361     * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but:
362     * <ul>
363     * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended
364     *    modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li>
365     * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li>
366     * </ul>
367     * @return extended modifier key used as the appropriate accelerator key for menu shortcuts
368     * @since 7539
369     */
370    public static int getMenuShortcutKeyMaskEx() {
371        return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK;
372    }
373
374    /**
375     * Sets a global font for all UI, replacing default font of current look and feel.
376     * @param name Font name. It is up to the caller to make sure the font exists
377     * @since 7896
378     * @throws IllegalArgumentException if name is null
379     */
380    public static void setUIFont(String name) {
381        CheckParameterUtil.ensureParameterNotNull(name, "name");
382        Main.info("Setting "+name+" as the default UI font");
383        Enumeration<?> keys = UIManager.getDefaults().keys();
384        while (keys.hasMoreElements()) {
385            Object key = keys.nextElement();
386            Object value = UIManager.get(key);
387            if (value != null && value instanceof FontUIResource) {
388                FontUIResource fui = (FontUIResource)value;
389                UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize()));
390            }
391        }
392    }
393}