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.Image;
014import java.awt.Stroke;
015import java.awt.Toolkit;
016import java.awt.Window;
017import java.awt.event.ActionListener;
018import java.awt.event.HierarchyEvent;
019import java.awt.event.HierarchyListener;
020import java.awt.image.FilteredImageSource;
021import java.lang.reflect.InvocationTargetException;
022import java.util.Arrays;
023import java.util.List;
024
025import javax.swing.GrayFilter;
026import javax.swing.Icon;
027import javax.swing.ImageIcon;
028import javax.swing.JOptionPane;
029import javax.swing.SwingUtilities;
030import javax.swing.Timer;
031
032import org.openstreetmap.josm.Main;
033import org.openstreetmap.josm.gui.ExtendedDialog;
034import org.openstreetmap.josm.tools.ImageProvider;
035
036/**
037 * basic gui utils
038 */
039public final class GuiHelper {
040    
041    private GuiHelper() {
042        // Hide default constructor for utils classes
043    }
044    
045    /**
046     * disable / enable a component and all its child components
047     */
048    public static void setEnabledRec(Container root, boolean enabled) {
049        root.setEnabled(enabled);
050        Component[] children = root.getComponents();
051        for (Component child : children) {
052            if(child instanceof Container) {
053                setEnabledRec((Container) child, enabled);
054            } else {
055                child.setEnabled(enabled);
056            }
057        }
058    }
059
060    public static void executeByMainWorkerInEDT(final Runnable task) {
061        Main.worker.submit(new Runnable() {
062            @Override
063            public void run() {
064                runInEDTAndWait(task);
065            }
066        });
067    }
068
069    public static void runInEDT(Runnable task) {
070        if (SwingUtilities.isEventDispatchThread()) {
071            task.run();
072        } else {
073            SwingUtilities.invokeLater(task);
074        }
075    }
076
077    public static void runInEDTAndWait(Runnable task) {
078        if (SwingUtilities.isEventDispatchThread()) {
079            task.run();
080        } else {
081            try {
082                SwingUtilities.invokeAndWait(task);
083            } catch (InterruptedException e) {
084                e.printStackTrace();
085            } catch (InvocationTargetException e) {
086                e.printStackTrace();
087            }
088        }
089    }
090
091    /**
092     * returns true if the user wants to cancel, false if they
093     * want to continue
094     */
095    public static final boolean warnUser(String title, String content, ImageIcon baseActionIcon, String continueToolTip) {
096        ExtendedDialog dlg = new ExtendedDialog(Main.parent,
097                title, new String[] {tr("Cancel"), tr("Continue")});
098        dlg.setContent(content);
099        dlg.setButtonIcons(new Icon[] {
100                ImageProvider.get("cancel"),
101                ImageProvider.overlay(
102                        ImageProvider.get("upload"),
103                        new ImageIcon(ImageProvider.get("warning-small").getImage().getScaledInstance(10 , 10, Image.SCALE_SMOOTH)),
104                        ImageProvider.OverlayPosition.SOUTHEAST)});
105        dlg.setToolTipTexts(new String[] {
106                tr("Cancel"),
107                continueToolTip});
108        dlg.setIcon(JOptionPane.WARNING_MESSAGE);
109        dlg.setCancelButton(1);
110        return dlg.showDialog().getValue() != 2;
111    }
112
113    /**
114     * Replies the disabled (grayed) version of the specified image.
115     * @param image The image to disable
116     * @return The disabled (grayed) version of the specified image, brightened by 20%.
117     * @since 5484
118     */
119    public static final Image getDisabledImage(Image image) {
120        return Toolkit.getDefaultToolkit().createImage(
121                new FilteredImageSource(image.getSource(), new GrayFilter(true, 20)));
122    }
123
124    /**
125     * Replies the disabled (grayed) version of the specified icon.
126     * @param icon The icon to disable
127     * @return The disabled (grayed) version of the specified icon, brightened by 20%.
128     * @since 5484
129     */
130    public static final ImageIcon getDisabledIcon(ImageIcon icon) {
131        return new ImageIcon(getDisabledImage(icon.getImage()));
132    }
133
134    /**
135     * Attaches a {@code HierarchyListener} to the specified {@code Component} that
136     * will set its parent dialog resizeable. Use it before a call to JOptionPane#showXXXXDialog
137     * to make it resizeable.
138     * @param pane The component that will be displayed
139     * @param minDimension The minimum dimension that will be set for the dialog. Ignored if null
140     * @return {@code pane}
141     * @since 5493
142     */
143    public static final Component prepareResizeableOptionPane(final Component pane, final Dimension minDimension) {
144        if (pane != null) {
145            pane.addHierarchyListener(new HierarchyListener() {
146                @Override
147                public void hierarchyChanged(HierarchyEvent e) {
148                    Window window = SwingUtilities.getWindowAncestor(pane);
149                    if (window instanceof Dialog) {
150                        Dialog dialog = (Dialog)window;
151                        if (!dialog.isResizable()) {
152                            dialog.setResizable(true);
153                            if (minDimension != null) {
154                                dialog.setMinimumSize(minDimension);
155                            }
156                        }
157                    }
158                }
159            });
160        }
161        return pane;
162    }
163
164    /**
165     * Schedules a new Timer to be run in the future (once or several times).
166     * @param initialDelay milliseconds for the initial and between-event delay if repeatable
167     * @param actionListener an initial listener; can be null
168     * @param repeats specify false to make the timer stop after sending its first action event
169     * @return The (started) timer.
170     * @since 5735
171     */
172    public static final Timer scheduleTimer(int initialDelay, ActionListener actionListener, boolean repeats) {
173        Timer timer = new Timer(initialDelay, actionListener);
174        timer.setRepeats(repeats);
175        timer.start();
176        return timer;
177    }
178
179    /**
180     * Return s new BasicStroke object with given thickness and style
181     * @param code = 3.5 -> thickness=3.5px; 3.5 10 5 -> thickness=3.5px, dashed: 10px filled + 5px empty
182     * @return stroke for drawing
183     */
184    public static Stroke getCustomizedStroke(String code) {
185        String[] s = code.trim().split("[^\\.0-9]+");
186
187        if (s.length==0) return new BasicStroke();
188        float w;
189        try {
190            w = Float.parseFloat(s[0]);
191        } catch (NumberFormatException ex) {
192            w = 1.0f;
193        }
194        if (s.length>1) {
195            float[] dash= new float[s.length-1];
196            float sumAbs = 0;
197            try {
198                for (int i=0; i<s.length-1; i++) {
199                   dash[i] = Float.parseFloat(s[i+1]);
200                   sumAbs += Math.abs(dash[i]);
201                }
202            } catch (NumberFormatException ex) {
203                Main.error("Error in stroke preference format: "+code);
204                dash = new float[]{5.0f};
205            }
206            if (sumAbs < 1e-1) {
207                Main.error("Error in stroke dash fomat (all zeros): "+code);
208                return new BasicStroke(w);
209            }
210            // dashed stroke
211            return new BasicStroke(w, BasicStroke.CAP_BUTT,
212                    BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
213        } else {
214            if (w>1) {
215                // thick stroke
216                return new BasicStroke(w, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
217            } else {
218                // thin stroke
219                return new BasicStroke(w);
220            }
221        }
222    }
223
224    /**
225     * Gets the font used to display JOSM title in about dialog and splash screen.
226     * @return By order or priority, the first font available in local fonts:
227     *         1. Helvetica Bold 20
228     *         2. Calibri Bold 23
229     *         3. Arial Bold 20
230     *         4. SansSerif Bold 20
231     * @since 5797
232     */
233    public static Font getTitleFont() {
234        List<String> fonts = Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
235        // Helvetica is the preferred choice but is not available by default on Windows
236        // (http://www.microsoft.com/typography/fonts/product.aspx?pid=161)
237        if (fonts.contains("Helvetica")) {
238            return new Font("Helvetica", Font.BOLD, 20);
239        // Calibri is the default Windows font since Windows Vista but is not available on older versions of Windows, where Arial is preferred
240        } else if (fonts.contains("Calibri")) {
241            return new Font("Calibri", Font.BOLD, 23);
242        } else if (fonts.contains("Arial")) {
243            return new Font("Arial", Font.BOLD, 20);
244        // No luck, nothing found, fallback to one of the 5 fonts provided with Java (Serif, SansSerif, Monospaced, Dialog, and DialogInput)
245        } else {
246            return new Font("SansSerif", Font.BOLD, 20);
247        }
248    }
249}