001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.Dialog.ModalityType;
008import java.awt.GraphicsEnvironment;
009import java.awt.event.ActionEvent;
010import java.awt.event.WindowAdapter;
011import java.awt.event.WindowEvent;
012import java.util.ArrayList;
013import java.util.Collection;
014import java.util.HashSet;
015import java.util.List;
016
017import javax.swing.AbstractAction;
018import javax.swing.Action;
019import javax.swing.Icon;
020import javax.swing.JButton;
021import javax.swing.JDialog;
022import javax.swing.JOptionPane;
023import javax.swing.event.ChangeEvent;
024import javax.swing.event.ChangeListener;
025
026import org.openstreetmap.josm.Main;
027import org.openstreetmap.josm.gui.help.HelpBrowser;
028import org.openstreetmap.josm.gui.help.HelpUtil;
029import org.openstreetmap.josm.gui.util.GuiHelper;
030import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
031import org.openstreetmap.josm.tools.ImageProvider;
032import org.openstreetmap.josm.tools.InputMapUtils;
033import org.openstreetmap.josm.tools.WindowGeometry;
034
035public final class HelpAwareOptionPane {
036
037    private HelpAwareOptionPane() {
038        // Hide default constructor for utils classes
039    }
040
041    public static class ButtonSpec {
042        public final String text;
043        public final Icon icon;
044        public final String tooltipText;
045        public final String helpTopic;
046        private boolean enabled;
047
048        private final Collection<ChangeListener> listeners = new HashSet<>();
049
050        /**
051         * Constructs a new {@code ButtonSpec}.
052         * @param text the button text
053         * @param icon the icon to display. Can be null
054         * @param tooltipText the tooltip text. Can be null.
055         * @param helpTopic the help topic. Can be null.
056         */
057        public ButtonSpec(String text, Icon icon, String tooltipText, String helpTopic) {
058            this(text, icon, tooltipText, helpTopic, true);
059        }
060
061        /**
062         * Constructs a new {@code ButtonSpec}.
063         * @param text the button text
064         * @param icon the icon to display. Can be null
065         * @param tooltipText the tooltip text. Can be null.
066         * @param helpTopic the help topic. Can be null.
067         * @param enabled the enabled status
068         * @since 5951
069         */
070        public ButtonSpec(String text, Icon icon, String tooltipText, String helpTopic, boolean enabled) {
071            this.text = text;
072            this.icon = icon;
073            this.tooltipText = tooltipText;
074            this.helpTopic = helpTopic;
075            setEnabled(enabled);
076        }
077
078        /**
079         * Determines if this button spec is enabled
080         * @return {@code true} if this button spec is enabled, {@code false} otherwise
081         * @since 6051
082         */
083        public final boolean isEnabled() {
084            return enabled;
085        }
086
087        /**
088         * Enables or disables this button spec, depending on the value of the parameter {@code b}.
089         * @param enabled if {@code true}, this button spec is enabled; otherwise this button spec is disabled
090         * @since 6051
091         */
092        public final void setEnabled(boolean enabled) {
093            if (this.enabled != enabled) {
094                this.enabled = enabled;
095                ChangeEvent event = new ChangeEvent(this);
096                for (ChangeListener listener : listeners) {
097                    listener.stateChanged(event);
098                }
099            }
100        }
101
102        private boolean addChangeListener(ChangeListener listener) {
103            return listener != null && listeners.add(listener);
104        }
105    }
106
107    private static class DefaultAction extends AbstractAction {
108        private final JDialog dialog;
109        private final JOptionPane pane;
110        private final int value;
111
112        DefaultAction(JDialog dialog, JOptionPane pane, int value) {
113            this.dialog = dialog;
114            this.pane = pane;
115            this.value = value;
116        }
117
118        @Override
119        public void actionPerformed(ActionEvent e) {
120            pane.setValue(value);
121            dialog.setVisible(false);
122        }
123    }
124
125    /**
126     * Creates the list buttons to be displayed in the option pane dialog.
127     *
128     * @param options the option. If null, just creates an OK button and a help button
129     * @param helpTopic the help topic. The context sensitive help of all buttons is equal
130     * to the context sensitive help of the whole dialog
131     * @return the list of buttons
132     */
133    private static List<JButton> createOptionButtons(ButtonSpec[] options, String helpTopic) {
134        List<JButton> buttons = new ArrayList<>();
135        if (options == null) {
136            JButton b = new JButton(tr("OK"));
137            b.setIcon(ImageProvider.get("ok"));
138            b.setToolTipText(tr("Click to close the dialog"));
139            b.setFocusable(true);
140            buttons.add(b);
141        } else {
142            for (final ButtonSpec spec: options) {
143                final JButton b = new JButton(spec.text);
144                b.setIcon(spec.icon);
145                b.setToolTipText(spec.tooltipText == null ? "" : spec.tooltipText);
146                if (helpTopic != null) {
147                    HelpUtil.setHelpContext(b, helpTopic);
148                }
149                b.setFocusable(true);
150                b.setEnabled(spec.isEnabled());
151                spec.addChangeListener(e -> b.setEnabled(spec.isEnabled()));
152                buttons.add(b);
153            }
154        }
155        return buttons;
156    }
157
158    /**
159     * Creates the help button
160     *
161     * @param helpTopic the help topic
162     * @return the help button
163     */
164    private static JButton createHelpButton(final String helpTopic) {
165        JButton b = new JButton(tr("Help"));
166        b.setIcon(ImageProvider.get("help"));
167        b.setToolTipText(tr("Show help information"));
168        HelpUtil.setHelpContext(b, helpTopic);
169        Action a = new AbstractAction() {
170            @Override
171            public void actionPerformed(ActionEvent e) {
172                HelpBrowser.setUrlForHelpTopic(helpTopic);
173            }
174        };
175        b.addActionListener(a);
176        InputMapUtils.enableEnter(b);
177        return b;
178    }
179
180    /**
181     * Displays an option dialog which is aware of a help context. If <code>helpTopic</code> isn't null,
182     * the dialog includes a "Help" button and launches the help browser if the user presses F1. If the
183     * user clicks on the "Help" button the option dialog remains open and JOSM launches the help
184     * browser.
185     *
186     * <code>helpTopic</code> is the trailing part of a JOSM online help URL, i.e. the part after the leading
187     * <code>https://josm.openstreetmap.de/wiki/Help</code>. It should start with a leading '/' and it
188     * may include an anchor after a '#'.
189     *
190     * <strong>Examples</strong>
191     * <ul>
192     *    <li>/Dialogs/RelationEditor</li>
193     *    <li>/Dialogs/RelationEditor#ConflictInData</li>
194     * </ul>
195     *
196     * In addition, the option buttons display JOSM icons, similar to ExtendedDialog.
197     *
198     * @param parentComponent the parent component
199     * @param msg the message
200     * @param title the title
201     * @param messageType the message type (see {@link JOptionPane})
202     * @param icon the icon to display. Can be null.
203     * @param options the list of options to display. Can be null.
204     * @param defaultOption the default option. Can be null.
205     * @param helpTopic the help topic. Can be null.
206     * @return the index of the selected option or {@link JOptionPane#CLOSED_OPTION}
207     */
208    public static int showOptionDialog(Component parentComponent, Object msg, String title, int messageType,
209            Icon icon, final ButtonSpec[] options, final ButtonSpec defaultOption, final String helpTopic) {
210        final List<JButton> buttons = createOptionButtons(options, helpTopic);
211        if (helpTopic != null) {
212            buttons.add(createHelpButton(helpTopic));
213        }
214
215        JButton defaultButton = null;
216        if (options != null && defaultOption != null) {
217            for (int i = 0; i < options.length; i++) {
218                if (options[i] == defaultOption) {
219                    defaultButton = buttons.get(i);
220                    break;
221                }
222            }
223        }
224
225        final JOptionPane pane = new JOptionPane(
226                msg instanceof String ? new JMultilineLabel((String) msg, true) : msg,
227                messageType,
228                JOptionPane.DEFAULT_OPTION,
229                icon,
230                buttons.toArray(),
231                defaultButton
232        );
233
234        // Log message. Useful for bug reports and unit tests
235        switch (messageType) {
236            case JOptionPane.ERROR_MESSAGE:
237                Main.error(title + " - " + msg);
238                break;
239            case JOptionPane.WARNING_MESSAGE:
240                Main.warn(title + " - " + msg);
241                break;
242            default:
243                Main.info(title + " - " + msg);
244        }
245
246        if (!GraphicsEnvironment.isHeadless()) {
247            doShowOptionDialog(parentComponent, title, options, defaultOption, helpTopic, buttons, pane);
248        }
249        return pane.getValue() instanceof Integer ? (Integer) pane.getValue() : JOptionPane.OK_OPTION;
250    }
251
252    private static void doShowOptionDialog(Component parentComponent, String title, final ButtonSpec[] options,
253            final ButtonSpec defaultOption, final String helpTopic, final List<JButton> buttons,
254            final JOptionPane pane) {
255        final JDialog dialog = new JDialog(
256                GuiHelper.getFrameForComponent(parentComponent),
257                title,
258                ModalityType.DOCUMENT_MODAL
259        );
260        dialog.setContentPane(pane);
261        dialog.addWindowListener(new WindowAdapter() {
262            @Override
263            public void windowClosing(WindowEvent e) {
264                pane.setValue(JOptionPane.CLOSED_OPTION);
265                super.windowClosed(e);
266            }
267
268            @Override
269            public void windowOpened(WindowEvent e) {
270                if (defaultOption != null && options != null && options.length > 0) {
271                    int i;
272                    for (i = 0; i < options.length; i++) {
273                        if (options[i] == defaultOption) {
274                            break;
275                        }
276                    }
277                    if (i >= options.length) {
278                        buttons.get(0).requestFocusInWindow();
279                    }
280                    buttons.get(i).requestFocusInWindow();
281                } else {
282                    buttons.get(0).requestFocusInWindow();
283                }
284            }
285        });
286        InputMapUtils.addEscapeAction(dialog.getRootPane(), new AbstractAction() {
287            @Override
288            public void actionPerformed(ActionEvent e) {
289                pane.setValue(JOptionPane.CLOSED_OPTION);
290                dialog.setVisible(false);
291            }
292        });
293
294        if (options != null) {
295            for (int i = 0; i < options.length; i++) {
296                final DefaultAction action = new DefaultAction(dialog, pane, i);
297                buttons.get(i).addActionListener(action);
298                InputMapUtils.addEnterAction(buttons.get(i), action);
299            }
300        } else {
301            final DefaultAction action = new DefaultAction(dialog, pane, 0);
302            buttons.get(0).addActionListener(action);
303            InputMapUtils.addEnterAction(buttons.get(0), action);
304        }
305
306        dialog.pack();
307        WindowGeometry.centerOnScreen(dialog.getSize()).applySafe(dialog);
308        if (helpTopic != null) {
309            HelpUtil.setHelpContext(dialog.getRootPane(), helpTopic);
310        }
311        dialog.setVisible(true);
312    }
313
314    /**
315     * Displays an option dialog which is aware of a help context.
316     *
317     * @param parentComponent the parent component
318     * @param msg the message
319     * @param title the title
320     * @param messageType the message type (see {@link JOptionPane})
321     * @param helpTopic the help topic. Can be null.
322     * @return the index of the selected option or {@link JOptionPane#CLOSED_OPTION}
323     * @see #showOptionDialog(Component, Object, String, int, Icon, ButtonSpec[], ButtonSpec, String)
324     */
325    public static int showOptionDialog(Component parentComponent, Object msg, String title, int messageType, String helpTopic) {
326        return showOptionDialog(parentComponent, msg, title, messageType, null, null, null, helpTopic);
327    }
328
329    /**
330     * Run it in Event Dispatch Thread.
331     * This version does not return anything, so it is more like {@code showMessageDialog}.
332     *
333     * It can be used, when you need to show a message dialog from a worker thread,
334     * e.g. from {@code PleaseWaitRunnable}.
335     *
336     * @param parentComponent the parent component
337     * @param msg the message
338     * @param title the title
339     * @param messageType the message type (see {@link JOptionPane})
340     * @param helpTopic the help topic. Can be null.
341     */
342    public static void showMessageDialogInEDT(final Component parentComponent, final Object msg, final String title,
343            final int messageType, final String helpTopic) {
344        GuiHelper.runInEDT(() -> showOptionDialog(parentComponent, msg, title, messageType, null, null, null, helpTopic));
345    }
346}