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.Color;
008import java.awt.Component;
009import java.awt.Container;
010import java.awt.Dialog;
011import java.awt.Dimension;
012import java.awt.Font;
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.Enumeration;
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.JComponent;
033import javax.swing.JLabel;
034import javax.swing.JOptionPane;
035import javax.swing.JPanel;
036import javax.swing.JScrollPane;
037import javax.swing.SwingUtilities;
038import javax.swing.Timer;
039import javax.swing.UIManager;
040import javax.swing.plaf.FontUIResource;
041
042import org.openstreetmap.josm.Main;
043import org.openstreetmap.josm.gui.ExtendedDialog;
044import org.openstreetmap.josm.gui.widgets.HtmlPanel;
045import org.openstreetmap.josm.tools.CheckParameterUtil;
046import org.openstreetmap.josm.tools.ColorHelper;
047import org.openstreetmap.josm.tools.GBC;
048import org.openstreetmap.josm.tools.ImageOverlay;
049import org.openstreetmap.josm.tools.ImageProvider;
050import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
051import org.openstreetmap.josm.tools.LanguageInfo;
052
053/**
054 * basic gui utils
055 */
056public final class GuiHelper {
057
058    private GuiHelper() {
059        // Hide default constructor for utils classes
060    }
061
062    /**
063     * disable / enable a component and all its child components
064     * @param root component
065     * @param enabled enabled state
066     */
067    public static void setEnabledRec(Container root, boolean enabled) {
068        root.setEnabled(enabled);
069        Component[] children = root.getComponents();
070        for (Component child : children) {
071            if (child instanceof Container) {
072                setEnabledRec((Container) child, enabled);
073            } else {
074                child.setEnabled(enabled);
075            }
076        }
077    }
078
079    public static void executeByMainWorkerInEDT(final Runnable task) {
080        Main.worker.submit(new Runnable() {
081            @Override
082            public void run() {
083                runInEDTAndWait(task);
084            }
085        });
086    }
087
088    /**
089     * Executes asynchronously a runnable in
090     * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
091     * @param task The runnable to execute
092     * @see SwingUtilities#invokeLater
093     */
094    public static void runInEDT(Runnable task) {
095        if (SwingUtilities.isEventDispatchThread()) {
096            task.run();
097        } else {
098            SwingUtilities.invokeLater(task);
099        }
100    }
101
102    /**
103     * Executes synchronously a runnable in
104     * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
105     * @param task The runnable to execute
106     * @see SwingUtilities#invokeAndWait
107     */
108    public static void runInEDTAndWait(Runnable task) {
109        if (SwingUtilities.isEventDispatchThread()) {
110            task.run();
111        } else {
112            try {
113                SwingUtilities.invokeAndWait(task);
114            } catch (InterruptedException | InvocationTargetException e) {
115                Main.error(e);
116            }
117        }
118    }
119
120    /**
121     * Executes synchronously a callable in
122     * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>
123     * and return a value.
124     * @param <V> the result type of method <tt>call</tt>
125     * @param callable The callable to execute
126     * @return The computed result
127     * @since 7204
128     */
129    public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) {
130        if (SwingUtilities.isEventDispatchThread()) {
131            try {
132                return callable.call();
133            } catch (Exception e) {
134                Main.error(e);
135                return null;
136            }
137        } else {
138            FutureTask<V> task = new FutureTask<>(callable);
139            SwingUtilities.invokeLater(task);
140            try {
141                return task.get();
142            } catch (InterruptedException | ExecutionException e) {
143                Main.error(e);
144                return null;
145            }
146        }
147    }
148
149    /**
150     * Warns user about a dangerous action requiring confirmation.
151     * @param title Title of dialog
152     * @param content Content of dialog
153     * @param baseActionIcon Unused? FIXME why is this parameter unused?
154     * @param continueToolTip Tooltip to display for "continue" button
155     * @return true if the user wants to cancel, false if they want to continue
156     */
157    public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) {
158        ExtendedDialog dlg = new ExtendedDialog(Main.parent,
159                title, new String[] {tr("Cancel"), tr("Continue")});
160        dlg.setContent(content);
161        dlg.setButtonIcons(new Icon[] {
162                    new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(),
163                    new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay(
164                            new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()});
165        dlg.setToolTipTexts(new String[] {
166                tr("Cancel"),
167                continueToolTip});
168        dlg.setIcon(JOptionPane.WARNING_MESSAGE);
169        dlg.setCancelButton(1);
170        return dlg.showDialog().getValue() != 2;
171    }
172
173    /**
174     * Notifies user about an error received from an external source as an HTML page.
175     * @param parent Parent component
176     * @param title Title of dialog
177     * @param message Message displayed at the top of the dialog
178     * @param html HTML content to display (real error message)
179     * @since 7312
180     */
181    public static void notifyUserHtmlError(Component parent, String title, String message, String html) {
182        JPanel p = new JPanel(new GridBagLayout());
183        p.add(new JLabel(message), GBC.eol());
184        p.add(new JLabel(tr("Received error page:")), GBC.eol());
185        JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html));
186        sp.setPreferredSize(new Dimension(640, 240));
187        p.add(sp, GBC.eol().fill(GBC.BOTH));
188
189        ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")});
190        ed.setButtonIcons(new String[] {"ok.png"});
191        ed.setContent(p);
192        ed.showDialog();
193    }
194
195    /**
196     * Replies the disabled (grayed) version of the specified image.
197     * @param image The image to disable
198     * @return The disabled (grayed) version of the specified image, brightened by 20%.
199     * @since 5484
200     */
201    public static Image getDisabledImage(Image image) {
202        return Toolkit.getDefaultToolkit().createImage(
203                new FilteredImageSource(image.getSource(), new GrayFilter(true, 20)));
204    }
205
206    /**
207     * Replies the disabled (grayed) version of the specified icon.
208     * @param icon The icon to disable
209     * @return The disabled (grayed) version of the specified icon, brightened by 20%.
210     * @since 5484
211     */
212    public static ImageIcon getDisabledIcon(ImageIcon icon) {
213        return new ImageIcon(getDisabledImage(icon.getImage()));
214    }
215
216    /**
217     * Attaches a {@code HierarchyListener} to the specified {@code Component} that
218     * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog
219     * to make it resizeable.
220     * @param pane The component that will be displayed
221     * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null
222     * @return {@code pane}
223     * @since 5493
224     */
225    public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) {
226        if (pane != null) {
227            pane.addHierarchyListener(new HierarchyListener() {
228                @Override
229                public void hierarchyChanged(HierarchyEvent e) {
230                    Window window = SwingUtilities.getWindowAncestor(pane);
231                    if (window instanceof Dialog) {
232                        Dialog dialog = (Dialog) window;
233                        if (!dialog.isResizable()) {
234                            dialog.setResizable(true);
235                            if (minDimension != null) {
236                                dialog.setMinimumSize(minDimension);
237                            }
238                        }
239                    }
240                }
241            });
242        }
243        return pane;
244    }
245
246    /**
247     * Schedules a new Timer to be run in the future (once or several times).
248     * @param initialDelay milliseconds for the initial and between-event delay if repeatable
249     * @param actionListener an initial listener; can be null
250     * @param repeats specify false to make the timer stop after sending its first action event
251     * @return The (started) timer.
252     * @since 5735
253     */
254    public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) {
255        Timer timer = new Timer(initialDelay, actionListener);
256        timer.setRepeats(repeats);
257        timer.start();
258        return timer;
259    }
260
261    /**
262     * Return s new BasicStroke object with given thickness and style
263     * @param code = 3.5 -&gt; thickness=3.5px; 3.5 10 5 -&gt; thickness=3.5px, dashed: 10px filled + 5px empty
264     * @return stroke for drawing
265     */
266    public static Stroke getCustomizedStroke(String code) {
267        String[] s = code.trim().split("[^\\.0-9]+");
268
269        if (s.length == 0) return new BasicStroke();
270        float w;
271        try {
272            w = Float.parseFloat(s[0]);
273        } catch (NumberFormatException ex) {
274            w = 1.0f;
275        }
276        if (s.length > 1) {
277            float[] dash = new float[s.length-1];
278            float sumAbs = 0;
279            try {
280                for (int i = 0; i < s.length-1; i++) {
281                   dash[i] = Float.parseFloat(s[i+1]);
282                   sumAbs += Math.abs(dash[i]);
283                }
284            } catch (NumberFormatException ex) {
285                Main.error("Error in stroke preference format: "+code);
286                dash = new float[]{5.0f};
287            }
288            if (sumAbs < 1e-1) {
289                Main.error("Error in stroke dash fomat (all zeros): "+code);
290                return new BasicStroke(w);
291            }
292            // dashed stroke
293            return new BasicStroke(w, BasicStroke.CAP_BUTT,
294                    BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
295        } else {
296            if (w > 1) {
297                // thick stroke
298                return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
299            } else {
300                // thin stroke
301                return new BasicStroke(w);
302            }
303        }
304    }
305
306    /**
307     * Gets the font used to display monospaced text in a component, if possible.
308     * @param component The component
309     * @return the font used to display monospaced text in a component, if possible
310     * @since 7896
311     */
312    public static Font getMonospacedFont(JComponent component) {
313        // Special font for Khmer script
314        if ("km".equals(LanguageInfo.getJOSMLocaleCode())) {
315            return component.getFont();
316        } else {
317            return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize());
318        }
319    }
320
321    /**
322     * Gets the font used to display JOSM title in about dialog and splash screen.
323     * @return title font
324     * @since 5797
325     */
326    public static Font getTitleFont() {
327        return new Font("SansSerif", Font.BOLD, 23);
328    }
329
330    /**
331     * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}.
332     * @param panel The component to embed
333     * @return the vertical scrollable {@code JScrollPane}
334     * @since 6666
335     */
336    public static JScrollPane embedInVerticalScrollPane(Component panel) {
337        return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
338    }
339
340    /**
341     * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts.
342     * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but:
343     * <ul>
344     * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended
345     *    modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li>
346     * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li>
347     * </ul>
348     * @return extended modifier key used as the appropriate accelerator key for menu shortcuts
349     * @since 7539
350     */
351    public static int getMenuShortcutKeyMaskEx() {
352        return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK;
353    }
354
355    /**
356     * Sets a global font for all UI, replacing default font of current look and feel.
357     * @param name Font name. It is up to the caller to make sure the font exists
358     * @throws IllegalArgumentException if name is null
359     * @since 7896
360     */
361    public static void setUIFont(String name) {
362        CheckParameterUtil.ensureParameterNotNull(name, "name");
363        Main.info("Setting "+name+" as the default UI font");
364        Enumeration<?> keys = UIManager.getDefaults().keys();
365        while (keys.hasMoreElements()) {
366            Object key = keys.nextElement();
367            Object value = UIManager.get(key);
368            if (value instanceof FontUIResource) {
369                FontUIResource fui = (FontUIResource) value;
370                UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize()));
371            }
372        }
373    }
374
375    /**
376     * Sets the background color for this component, and adjust the foreground color so the text remains readable.
377     * @param c component
378     * @param background background color
379     * @since 9223
380     */
381    public static void setBackgroundReadable(JComponent c, Color background) {
382        c.setBackground(background);
383        c.setForeground(ColorHelper.getForegroundColor(background));
384    }
385}