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.DisplayMode;
013import java.awt.Font;
014import java.awt.GraphicsDevice;
015import java.awt.GraphicsEnvironment;
016import java.awt.GridBagLayout;
017import java.awt.Image;
018import java.awt.Stroke;
019import java.awt.Toolkit;
020import java.awt.Window;
021import java.awt.datatransfer.Clipboard;
022import java.awt.event.ActionListener;
023import java.awt.event.HierarchyEvent;
024import java.awt.event.HierarchyListener;
025import java.awt.event.KeyEvent;
026import java.awt.image.FilteredImageSource;
027import java.lang.reflect.InvocationTargetException;
028import java.util.Enumeration;
029import java.util.EventObject;
030import java.util.concurrent.Callable;
031import java.util.concurrent.ExecutionException;
032import java.util.concurrent.FutureTask;
033
034import javax.swing.GrayFilter;
035import javax.swing.Icon;
036import javax.swing.ImageIcon;
037import javax.swing.JComponent;
038import javax.swing.JLabel;
039import javax.swing.JOptionPane;
040import javax.swing.JPanel;
041import javax.swing.JScrollPane;
042import javax.swing.Scrollable;
043import javax.swing.SwingUtilities;
044import javax.swing.Timer;
045import javax.swing.UIManager;
046import javax.swing.plaf.FontUIResource;
047
048import org.openstreetmap.josm.Main;
049import org.openstreetmap.josm.gui.ExtendedDialog;
050import org.openstreetmap.josm.gui.widgets.HtmlPanel;
051import org.openstreetmap.josm.tools.CheckParameterUtil;
052import org.openstreetmap.josm.tools.ColorHelper;
053import org.openstreetmap.josm.tools.GBC;
054import org.openstreetmap.josm.tools.ImageOverlay;
055import org.openstreetmap.josm.tools.ImageProvider;
056import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
057import org.openstreetmap.josm.tools.LanguageInfo;
058
059/**
060 * basic gui utils
061 */
062public final class GuiHelper {
063
064    private GuiHelper() {
065        // Hide default constructor for utils classes
066    }
067
068    /**
069     * disable / enable a component and all its child components
070     * @param root component
071     * @param enabled enabled state
072     */
073    public static void setEnabledRec(Container root, boolean enabled) {
074        root.setEnabled(enabled);
075        Component[] children = root.getComponents();
076        for (Component child : children) {
077            if (child instanceof Container) {
078                setEnabledRec((Container) child, enabled);
079            } else {
080                child.setEnabled(enabled);
081            }
082        }
083    }
084
085    public static void executeByMainWorkerInEDT(final Runnable task) {
086        Main.worker.submit(new Runnable() {
087            @Override
088            public void run() {
089                runInEDTAndWait(task);
090            }
091        });
092    }
093
094    /**
095     * Executes asynchronously a runnable in
096     * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
097     * @param task The runnable to execute
098     * @see SwingUtilities#invokeLater
099     */
100    public static void runInEDT(Runnable task) {
101        if (SwingUtilities.isEventDispatchThread()) {
102            task.run();
103        } else {
104            SwingUtilities.invokeLater(task);
105        }
106    }
107
108    /**
109     * Executes synchronously a runnable in
110     * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>.
111     * @param task The runnable to execute
112     * @see SwingUtilities#invokeAndWait
113     */
114    public static void runInEDTAndWait(Runnable task) {
115        if (SwingUtilities.isEventDispatchThread()) {
116            task.run();
117        } else {
118            try {
119                SwingUtilities.invokeAndWait(task);
120            } catch (InterruptedException | InvocationTargetException e) {
121                Main.error(e);
122            }
123        }
124    }
125
126    /**
127     * Executes synchronously a callable in
128     * <a href="http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html">Event Dispatch Thread</a>
129     * and return a value.
130     * @param <V> the result type of method <tt>call</tt>
131     * @param callable The callable to execute
132     * @return The computed result
133     * @since 7204
134     */
135    public static <V> V runInEDTAndWaitAndReturn(Callable<V> callable) {
136        if (SwingUtilities.isEventDispatchThread()) {
137            try {
138                return callable.call();
139            } catch (Exception e) {
140                Main.error(e);
141                return null;
142            }
143        } else {
144            FutureTask<V> task = new FutureTask<>(callable);
145            SwingUtilities.invokeLater(task);
146            try {
147                return task.get();
148            } catch (InterruptedException | ExecutionException e) {
149                Main.error(e);
150                return null;
151            }
152        }
153    }
154
155    /**
156     * Warns user about a dangerous action requiring confirmation.
157     * @param title Title of dialog
158     * @param content Content of dialog
159     * @param baseActionIcon Unused? FIXME why is this parameter unused?
160     * @param continueToolTip Tooltip to display for "continue" button
161     * @return true if the user wants to cancel, false if they want to continue
162     */
163    public static boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) {
164        ExtendedDialog dlg = new ExtendedDialog(Main.parent,
165                title, new String[] {tr("Cancel"), tr("Continue")});
166        dlg.setContent(content);
167        dlg.setButtonIcons(new Icon[] {
168                    new ImageProvider("cancel").setMaxSize(ImageSizes.LARGEICON).get(),
169                    new ImageProvider("upload").setMaxSize(ImageSizes.LARGEICON).addOverlay(
170                            new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()});
171        dlg.setToolTipTexts(new String[] {
172                tr("Cancel"),
173                continueToolTip});
174        dlg.setIcon(JOptionPane.WARNING_MESSAGE);
175        dlg.setCancelButton(1);
176        return dlg.showDialog().getValue() != 2;
177    }
178
179    /**
180     * Notifies user about an error received from an external source as an HTML page.
181     * @param parent Parent component
182     * @param title Title of dialog
183     * @param message Message displayed at the top of the dialog
184     * @param html HTML content to display (real error message)
185     * @since 7312
186     */
187    public static void notifyUserHtmlError(Component parent, String title, String message, String html) {
188        JPanel p = new JPanel(new GridBagLayout());
189        p.add(new JLabel(message), GBC.eol());
190        p.add(new JLabel(tr("Received error page:")), GBC.eol());
191        JScrollPane sp = embedInVerticalScrollPane(new HtmlPanel(html));
192        sp.setPreferredSize(new Dimension(640, 240));
193        p.add(sp, GBC.eol().fill(GBC.BOTH));
194
195        ExtendedDialog ed = new ExtendedDialog(parent, title, new String[] {tr("OK")});
196        ed.setButtonIcons(new String[] {"ok.png"});
197        ed.setContent(p);
198        ed.showDialog();
199    }
200
201    /**
202     * Replies the disabled (grayed) version of the specified image.
203     * @param image The image to disable
204     * @return The disabled (grayed) version of the specified image, brightened by 20%.
205     * @since 5484
206     */
207    public static Image getDisabledImage(Image image) {
208        return Toolkit.getDefaultToolkit().createImage(
209                new FilteredImageSource(image.getSource(), new GrayFilter(true, 20)));
210    }
211
212    /**
213     * Replies the disabled (grayed) version of the specified icon.
214     * @param icon The icon to disable
215     * @return The disabled (grayed) version of the specified icon, brightened by 20%.
216     * @since 5484
217     */
218    public static ImageIcon getDisabledIcon(ImageIcon icon) {
219        return new ImageIcon(getDisabledImage(icon.getImage()));
220    }
221
222    /**
223     * Attaches a {@code HierarchyListener} to the specified {@code Component} that
224     * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog
225     * to make it resizeable.
226     * @param pane The component that will be displayed
227     * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null
228     * @return {@code pane}
229     * @since 5493
230     */
231    public static Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) {
232        if (pane != null) {
233            pane.addHierarchyListener(new HierarchyListener() {
234                @Override
235                public void hierarchyChanged(HierarchyEvent e) {
236                    Window window = SwingUtilities.getWindowAncestor(pane);
237                    if (window instanceof Dialog) {
238                        Dialog dialog = (Dialog) window;
239                        if (!dialog.isResizable()) {
240                            dialog.setResizable(true);
241                            if (minDimension != null) {
242                                dialog.setMinimumSize(minDimension);
243                            }
244                        }
245                    }
246                }
247            });
248        }
249        return pane;
250    }
251
252    /**
253     * Schedules a new Timer to be run in the future (once or several times).
254     * @param initialDelay milliseconds for the initial and between-event delay if repeatable
255     * @param actionListener an initial listener; can be null
256     * @param repeats specify false to make the timer stop after sending its first action event
257     * @return The (started) timer.
258     * @since 5735
259     */
260    public static Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) {
261        Timer timer = new Timer(initialDelay, actionListener);
262        timer.setRepeats(repeats);
263        timer.start();
264        return timer;
265    }
266
267    /**
268     * Return s new BasicStroke object with given thickness and style
269     * @param code = 3.5 -&gt; thickness=3.5px; 3.5 10 5 -&gt; thickness=3.5px, dashed: 10px filled + 5px empty
270     * @return stroke for drawing
271     */
272    public static Stroke getCustomizedStroke(String code) {
273        String[] s = code.trim().split("[^\\.0-9]+");
274
275        if (s.length == 0) return new BasicStroke();
276        float w;
277        try {
278            w = Float.parseFloat(s[0]);
279        } catch (NumberFormatException ex) {
280            w = 1.0f;
281        }
282        if (s.length > 1) {
283            float[] dash = new float[s.length-1];
284            float sumAbs = 0;
285            try {
286                for (int i = 0; i < s.length-1; i++) {
287                   dash[i] = Float.parseFloat(s[i+1]);
288                   sumAbs += Math.abs(dash[i]);
289                }
290            } catch (NumberFormatException ex) {
291                Main.error("Error in stroke preference format: "+code);
292                dash = new float[]{5.0f};
293            }
294            if (sumAbs < 1e-1) {
295                Main.error("Error in stroke dash fomat (all zeros): "+code);
296                return new BasicStroke(w);
297            }
298            // dashed stroke
299            return new BasicStroke(w, BasicStroke.CAP_BUTT,
300                    BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
301        } else {
302            if (w > 1) {
303                // thick stroke
304                return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
305            } else {
306                // thin stroke
307                return new BasicStroke(w);
308            }
309        }
310    }
311
312    /**
313     * Gets the font used to display monospaced text in a component, if possible.
314     * @param component The component
315     * @return the font used to display monospaced text in a component, if possible
316     * @since 7896
317     */
318    public static Font getMonospacedFont(JComponent component) {
319        // Special font for Khmer script
320        if ("km".equals(LanguageInfo.getJOSMLocaleCode())) {
321            return component.getFont();
322        } else {
323            return new Font("Monospaced", component.getFont().getStyle(), component.getFont().getSize());
324        }
325    }
326
327    /**
328     * Gets the font used to display JOSM title in about dialog and splash screen.
329     * @return title font
330     * @since 5797
331     */
332    public static Font getTitleFont() {
333        return new Font("SansSerif", Font.BOLD, 23);
334    }
335
336    /**
337     * Embeds the given component into a new vertical-only scrollable {@code JScrollPane}.
338     * @param panel The component to embed
339     * @return the vertical scrollable {@code JScrollPane}
340     * @since 6666
341     */
342    public static JScrollPane embedInVerticalScrollPane(Component panel) {
343        return new JScrollPane(panel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
344    }
345
346    /**
347     * Set the default unit increment for a {@code JScrollPane}.
348     *
349     * This fixes slow mouse wheel scrolling when the content of the {@code JScrollPane}
350     * is a {@code JPanel} or other component that does not implement the {@link Scrollable}
351     * interface.
352     * The default unit increment is 1 pixel. Multiplied by the number of unit increments
353     * per mouse wheel "click" (platform dependent, usually 3), this makes a very
354     * sluggish mouse wheel experience.
355     * This methods sets the unit increment to a larger, more reasonable value.
356     * @param sp the scroll pane
357     * @return the scroll pane (same object) with fixed unit increment
358     * @throws IllegalArgumentException if the component inside of the scroll pane
359     * implements the {@code Scrollable} interface ({@code JTree}, {@code JLayer},
360     * {@code JList}, {@code JTextComponent} and {@code JTable})
361     */
362    public static JScrollPane setDefaultIncrement(JScrollPane sp) {
363        if (sp.getViewport().getView() instanceof Scrollable) {
364            throw new IllegalArgumentException();
365        }
366        sp.getVerticalScrollBar().setUnitIncrement(10);
367        sp.getHorizontalScrollBar().setUnitIncrement(10);
368        return sp;
369    }
370
371    /**
372     * Returns extended modifier key used as the appropriate accelerator key for menu shortcuts.
373     * It is advised everywhere to use {@link Toolkit#getMenuShortcutKeyMask()} to get the cross-platform modifier, but:
374     * <ul>
375     * <li>it returns KeyEvent.CTRL_MASK instead of KeyEvent.CTRL_DOWN_MASK. We used the extended
376     *    modifier for years, and Oracle recommends to use it instead, so it's best to keep it</li>
377     * <li>the method throws a HeadlessException ! So we would need to handle it for unit tests anyway</li>
378     * </ul>
379     * @return extended modifier key used as the appropriate accelerator key for menu shortcuts
380     * @since 7539
381     */
382    public static int getMenuShortcutKeyMaskEx() {
383        return Main.isPlatformOsx() ? KeyEvent.META_DOWN_MASK : KeyEvent.CTRL_DOWN_MASK;
384    }
385
386    /**
387     * Sets a global font for all UI, replacing default font of current look and feel.
388     * @param name Font name. It is up to the caller to make sure the font exists
389     * @throws IllegalArgumentException if name is null
390     * @since 7896
391     */
392    public static void setUIFont(String name) {
393        CheckParameterUtil.ensureParameterNotNull(name, "name");
394        Main.info("Setting "+name+" as the default UI font");
395        Enumeration<?> keys = UIManager.getDefaults().keys();
396        while (keys.hasMoreElements()) {
397            Object key = keys.nextElement();
398            Object value = UIManager.get(key);
399            if (value instanceof FontUIResource) {
400                FontUIResource fui = (FontUIResource) value;
401                UIManager.put(key, new FontUIResource(name, fui.getStyle(), fui.getSize()));
402            }
403        }
404    }
405
406    /**
407     * Sets the background color for this component, and adjust the foreground color so the text remains readable.
408     * @param c component
409     * @param background background color
410     * @since 9223
411     */
412    public static void setBackgroundReadable(JComponent c, Color background) {
413        c.setBackground(background);
414        c.setForeground(ColorHelper.getForegroundColor(background));
415    }
416
417    /**
418     * Gets the size of the screen. On systems with multiple displays, the primary display is used.
419     * This method returns always 800x600 in headless mode (useful for unit tests).
420     * @return the size of this toolkit's screen, in pixels, or 800x600
421     * @see Toolkit#getScreenSize
422     * @since 9576
423     */
424    public static Dimension getScreenSize() {
425        return GraphicsEnvironment.isHeadless() ? new Dimension(800, 600) : Toolkit.getDefaultToolkit().getScreenSize();
426    }
427
428    /**
429     * Gets the size of the screen. On systems with multiple displays,
430     * contrary to {@link #getScreenSize()}, the biggest display is used.
431     * This method returns always 800x600 in headless mode (useful for unit tests).
432     * @return the size of maximum screen, in pixels, or 800x600
433     * @see Toolkit#getScreenSize
434     * @since 9576
435     */
436
437    public static Dimension getMaxiumScreenSize() {
438        if (GraphicsEnvironment.isHeadless()) {
439            return new Dimension(800, 600);
440        }
441
442        int height = 0;
443        int width = 0;
444        for (GraphicsDevice gd: GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
445            DisplayMode dm = gd.getDisplayMode();
446            height = Math.max(height, dm.getHeight());
447            width = Math.max(width, dm.getWidth());
448        }
449        if (height == 0 || width == 0) {
450            return new Dimension(800, 600);
451        }
452        return new Dimension(width, height);
453    }
454
455    /**
456     * Gets the singleton instance of the system selection as a <code>Clipboard</code> object.
457     * This allows an application to read and modify the current, system-wide selection.
458     * @return the system selection as a <code>Clipboard</code>, or <code>null</code> if the native platform does not
459     *         support a system selection <code>Clipboard</code> or if GraphicsEnvironment.isHeadless() returns true
460     * @see Toolkit#getSystemSelection
461     * @since 9576
462     */
463    public static Clipboard getSystemSelection() {
464        return GraphicsEnvironment.isHeadless() ? null : Toolkit.getDefaultToolkit().getSystemSelection();
465    }
466
467    /**
468     * Returns the first <code>Window</code> ancestor of event source, or
469     * {@code null} if event source is not a component contained inside a <code>Window</code>.
470     * @param e event object
471     * @return a Window, or {@code null}
472     * @since 9916
473     */
474    public static Window getWindowAncestorFor(EventObject e) {
475        return e != null && e.getSource() instanceof Component ? SwingUtilities.getWindowAncestor((Component) e.getSource()) : null;
476    }
477}